Local Llama OdooBot Chat Companion
by okkype@gmail.com https://linkedin.com/in/okky-permana-sihipo$ 30.00
| Availability |
Odoo Online
Odoo.sh
On Premise
|
| Odoo Apps Dependencies |
Discuss (mail)
|
| Lines of code | 458 |
| Technical Name |
llama_cpp_ok |
| License | OPL-1 |
| Website | https://linkedin.com/in/okky-permana-sihipo |
| Availability |
Odoo Online
Odoo.sh
On Premise
|
| Odoo Apps Dependencies |
Discuss (mail)
|
| Lines of code | 458 |
| Technical Name |
llama_cpp_ok |
| License | OPL-1 |
| Website | https://linkedin.com/in/okky-permana-sihipo |
Materi Pelatihan Odoo 18 & Offline Llama
Panduan Teoretis & Praktis Terintegrasi: Pengaturan Lingkungan Virtual Multi-OS, Caching AI Lokal, Sandbox Abstract Syntax Tree (AST), dan Automated Testing
ð¡ï¸ 100% Offline AI
Inference lokal dengan model Qwen2.5-Coder GGUF yang berjalan di komputer lokal tanpa membutuhkan API key luar atau transfer data internet.
âï¸ AST Sandbox
Mencegah eksekusi Python berbahaya di server dengan menganalisis syntax tree sebelum kode ORM dievaluasi.
𧪠ORM Access
Memberikan kemampuan bagi AI OdooBot untuk memproses recordset Odoo secara dinamis berbasis masukan bahasa alami.
Daftar Isi Materi Pelatihan
Detail Bab & Sub-Bab Pembelajaran
Persiapan Lingkungan Pengembangan & Penyediaan Model GGUF Offline
Fokus Bab: Menyiapkan compiler C++ di sistem lokal, membangun conda virtual environment yang kompatibel dengan Odoo 18, mengompilasi python binding di berbagai OS (Linux, macOS, Windows), dan mengelola path model secara modular.
1.1 Pembuatan & Manajemen Conda Environment Odoo 18 di Berbagai OS
Odoo 18 menggunakan berbagai library biner Python yang spesifik. Penggunaan Conda memastikan dependensi binary C/C++ tidak bercampur dengan interpreter Python bawaan OS. Berikut adalah panduan setup environment untuk masing-masing sistem operasi:
Linux & macOS
Buka terminal Unix Anda, buat environment baru berbasis Python 3.10, dan aktifkan:
conda create -n odoo-18 python=3.10 -y conda activate odoo-18
Windows
Buka Anaconda Prompt atau Miniconda Prompt dan ketikkan:
conda create -n odoo-18 python=3.10 -y conda activate odoo-18
1.2 Kompilasi & Instalasi Library llama-cpp-python Lintas Sistem Operasi
Library llama-cpp-python memanggil biner native C++ dari `llama.cpp`. Konfigurasi dan compiler yang dibutuhkan berbeda secara signifikan di setiap sistem operasi:
Linux
Linux menggunakan compiler GCC / G++. Pastikan paket build-essential terpasang.
# Untuk standard CPU pip install llama-cpp-python --no-cache-dir # Akselerasi GPU CUDA (Nvidia) CUDACXX=/usr/local/cuda/bin/nvcc CMAKE_ARGS="-GGML_CUDA=on" FORCE_CMAKE=1 pip install llama-cpp-python --no-cache-dir
macOS
macOS menggunakan compiler Clang dari Xcode CLI. Pada Apple Silicon, biner dikompilasi menggunakan Metal API.
# Instal Xcode CLI Tools terlebih dahulu di Terminal xcode-select --install # Instalasi dengan akselerasi GPU Metal (Native pada Apple Silicon) CMAKE_ARGS="-GGML_METAL=on" FORCE_CMAKE=1 pip install llama-cpp-python --no-cache-dir
Windows
Windows membutuhkan compiler C++ Visual Studio (MSVC).
# Untuk standard CPU di Windows pip install llama-cpp-python --no-cache-dir # Akselerasi GPU CUDA di Windows (Jalankan di PowerShell Administrator) $env:CMAKE_ARGS="-GGML_CUDA=on" $env:FORCE_CMAKE="1" pip install llama-cpp-python --no-cache-dir
1.3 Pengelolaan Direktori Penyimpanan Model GGUF Lokal & Portabilitas Path
Agar modul portabel, kita menyimpan model biner di sub-direktori internal addon kita yaitu models_local/.
Modul ini menggunakan Qwen 2.5 Coder 1.5B Instruct (Q4_K_M) secara offline. Model ini hanya membutuhkan ~1.2 GB RAM/VRAM, sangat portabel dan aman dijalankan pada server lokal berspesifikasi standar.
Unix (Linux & macOS):
mkdir -p llama_cpp_ok/models_local/ mv qwen2.5-coder-1.5b-instruct-q4_k_m.gguf llama_cpp_ok/models_local/
Windows (PowerShell):
New-Item -ItemType Directory -Force -Path "llama_cpp_ok\models_local" Move-Item -Path "qwen2.5-coder-1.5b-instruct-q4_k_m.gguf" -Destination "llama_cpp_ok\models_local\"
Desain Model Konfigurasi & Warisan Tampilan Odoo Settings
Fokus Bab: Mempelajari arsitektur model transient Odoo, cara memetakan isian settings ke database parameter sistem (ir.config_parameter), memodifikasi tampilan General Settings XML, dan menulis nilai default noupdate.
2.1 Model Transient res.config.settings & Parameter Sistem
Odoo mengelola pengaturan umum menggunakan kelas turunan res.config.settings yang bertipe TransientModel. Nilai input yang dimasukkan user disimpan secara permanen di database Odoo pada tabel ir.config_parameter.
Lokasi file: models/res_config_settings.py
class ResConfigSettings(models.TransientModel): _inherit = "res.config.settings" ai_odoobot_enabled = fields.Selection( selection=[("disabled", "Disabled"), ("enabled", "Enabled")], string="AI OdooBot Mode", config_parameter="llama_cpp_ok.ai_odoobot_enabled", default="disabled", ) ai_model_path = fields.Char( string="AI GGUF Model Path", config_parameter="llama_cpp_ok.ai_model_path", default="models_local/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf", ) ai_thread_count = fields.Integer( string="AI CPU Threads", config_parameter="llama_cpp_ok.ai_thread_count", default="4", ) ai_context_window = fields.Integer( string="AI Context Window Size", config_parameter="llama_cpp_ok.ai_context_window", default="4096", ) ai_model_blacklist = fields.Char( string="AI Model Blacklist", config_parameter="llama_cpp_ok.ai_model_blacklist", default="res.users,res.groups,ir.config_parameter,ir.actions.server,ir.cron", )
2.2 Warisan Tampilan (Inheritance View XML) di General Settings
Di Odoo 18, struktur form views General Settings menggunakan pembungkus tag baru, yaitu `` dan `` (menggantikan struktur sheet/div lama dari Odoo 17). Kita melakukan inheritance ke view `mail.res_config_settings_view_form` menggunakan XPath expressions.
Lokasi file: views/res_config_settings_views.xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.llama</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="mail.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//form" position="inside">
<app string="Discuss" name="llama_cpp_ok">
<block title="Offline Llama AI OdooBot">
<setting title="AI Settings" string="Configure AI OdooBot Server">
<div class="content-group">
<field name="ai_odoobot_enabled"/>
<field name="ai_model_path" class="w-100" style="width: 100%;"/>
<field name="ai_thread_count"/>
<field name="ai_context_window"/>
<field name="ai_model_blacklist" style="width: 100%;"/>
</div>
</setting>
</block>
</app>
</xpath>
</field>
</record>
</odoo>
2.3 Registrasi Default System Parameters via XML Noupdate
Lokasi file: data/llama_config_data.xml
<record id="param_ai_model_path" model="ir.config_parameter">
<field name="key">llama_cpp_ok.ai_model_path</field>
<field name="value">models_local/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf</field>
</record>
Inti Mesin Inferensi Llama & Proteksi Sandbox AST
Fokus Bab: Mengelola alokasi memori C++ native, mendesain caching singleton yang efisien untuk meminimalkan beban RAM server, merancang sistem keamanan tingkat tinggi menggunakan penelusuran Abstract Syntax Tree (AST), serta mengeksekusi kode Odoo ORM secara terisolasi lintas platform.
3.1 Caching Memori AI Terisolasi & Garbage Collection
Dalam memproses model bahasa besar (LLM) secara lokal, pemuatan GGUF ditangani oleh C++ library (llama.cpp) melalui binding `ctypes`. Ini membagi alokasi memori menjadi dua wilayah:
- 1. Python Managed Heap (VM RAM): Mengelola wrapper class Python, objek config transient settings, metadata pemanggilan thread, dan pointer biner ctypes.
- 2. Native C++ Heap (Physical RAM/VRAM): Menyimpan bobot tensor model biner yang di-load dan alokasi array KV (Key-Value) Cache untuk penampung konteks percakapan.
Jika kita menginisialisasi ulang objek Llama pada setiap request pesan, alokasi memori biner C++ lama tidak akan terhapus secara otomatis. Kita harus menggunakan Caching Singleton.
[Request Input Chat Baru]
â
â¼
[Cek Cache _LLM_CACHE] ââ(Ada)ââ⺠[Gunakan Instance LLM C++ Aktif]
â
(Tidak)
â¼
[Hapus Pointer Model Lama di Cache]
â
â¼
[del _LLM_CACHE[key]] ââ⺠Memicu ctypes destructor (llama_free_model)
â
â¼
[gc.collect()] ââ⺠Bersihkan sisa referensi Python Heap
â
â¼
[Inisialisasi Model Baru] ââ⺠Alokasikan RAM/VRAM Fisik Bersih
Untuk membersihkan cache model lama secara total saat konfigurasi diubah, kita harus menghapus pointer referensi menggunakan kata kunci
del, memutus keterikatan objek dari dictionary cache global _LLM_CACHE, lalu secara eksplisit memanggil Garbage Collector Python melalui gc.collect().
Panduan Sintaksis & Inisiasi API llama-cpp-python:
from llama_cpp import Llama # 1. Inisialisasi Model Offline llm = Llama( model_path="models_local/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf", n_ctx=2048, # Context Window Size n_threads=4, # Core CPU fisik yang digunakan n_gpu_layers=0, # Layers yang ditaruh ke VRAM (0 jika CPU-only) verbose=False # Mematikan logs biner C++ verbose ) # 2. Mengirimkan Instruksi via API Chat Completion response = llm.create_chat_completion( messages=[ {"role": "system", "content": "Kamu adalah OdooBot..."}, {"role": "user", "content": "hitung 50 dikali 2"} ], max_tokens=1024, # Batas token output generator temperature=0.1, # Nilai deterministik rendah untuk coding top_p=0.9, # Threshold sampling probabilitas stream=False # Membaca output instan secara penuh ) # 3. Ekstraksi Hasil Output Teks output_text = response["choices"][0]["message"]["content"].strip()
3.2 Validasi Keamanan Kode Python menggunakan Abstract Syntax Tree (AST)
OdooBot AI dibekali kemampuan menerjemahkan perintah bahasa alami menjadi kode program ORM Odoo. Sebagai pertahanan utama, modul llama_cpp_ok mengimplementasikan Abstract Syntax Tree (AST) Security Sandbox.
for node in ast.walk(tree): # 1. Blokir Import secara total if isinstance(node, (ast.Import, ast.ImportFrom)): raise ValueError("Import statements are strictly prohibited.") # 2. Blokir Akses Atribut Dunder (Double Underscore) if isinstance(node, ast.Attribute): if node.attr.startswith("__") or node.attr.endswith("__"): raise ValueError("Double underscore attribute access is blocked.") # Blokir akses module namespace langsung if isinstance(node.value, ast.Name) and node.value.id in forbidden_modules: raise ValueError("Direct module namespace access is blocked.") # 3. Blokir Pemanggilan Fungsi Built-in Berbahaya if isinstance(node, ast.Call): func = node.func if isinstance(func, ast.Name) and func.id in forbidden_builtins: raise ValueError("Call to dangerous builtin function is prohibited.")
| Tipe Node AST | Status Keamanan | Motif & Penjelasan Perlindungan |
|---|---|---|
| ast.Import / ast.ImportFrom | BLOCKED | Mencegah pemuatan modul OS bawaan seperti os untuk menghentikan akses ke shell server. |
| ast.Attribute (Dunder "__") | BLOCKED | Menolak akses ke atribut internal (seperti __subclasses__) yang biasa digunakan dalam bypass sandbox. |
| ast.Call (Dangerous Builtins) | BLOCKED | Memblokir pemanggilan fungsi global bawaan berbahaya seperti eval() atau exec(). |
| ast.Assign / ast.BinOp | ALLOWED | Mengizinkan deklarasi variabel kalkulasi standar dan operasi aritmatika dasar. |
| ast.Call (Odoo ORM Search/Read) | ALLOWED | Mengizinkan pencarian database standar Odoo, namun melarang pemanggilan database jika target model di-blacklist. |
Contoh Parsing AST Kode ORM:
# Kode Input asli: result = env['res.partner'].search([]) # Di-parsing oleh AST Parser menjadi pohon objek berikut: Module( body=[ Assign( targets=[Name(id='result', ctx=Store())], value=Call( func=Attribute( value=Subscript( value=Name(id='env', ctx=Load()), slice=Constant(value='res.partner') ), attr='search' ), args=[List(elts=[])] ) ) ] )
3.3 Eksekusi Sandboxed Python & Parser Output HTML Lintas OS
Setelah string kode lolos dari validasi struktur pohon AST, kode tersebut dieksekusi di dalam lingkungan runtime Python yang terbatas (sandbox namespace):
globals_dict = {
"env": self.env,
"result": None
}
_logger.info("Local Llama Sandbox: Executing code...")
exec(code, globals_dict)
execution_result = globals_dict.get("result")
Untuk memastikan fungsionalitas ini berjalan di semua OS host, penentuan path file model harus diselesaikan secara dinamis. Modul ini menggunakan kode berikut untuk menggabungkan lokasi path secara independen terhadap sistem operasi (cross-platform):
# os.path secara otomatis menggunakan backslash (\) di Windows dan forward-slash (/) di Unix
addon_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
full_model_path = os.path.join(addon_dir, relative_model_path)
Intersepsi Pesan Discuss App & Alur Pengalihan OdooBot
Fokus Bab: Memodifikasi pipeline pesan Odoo Discuss, menangani chat direct message (DM) & mentions, melakukan sanitasi input string, dan menerapkan aturan fallback ke pipeline standar.
4.1 Override Pipeline _get_answer Pada Model mail.bot
Odoo menggunakan model mail.bot untuk mengendalikan logika interaksi otomatis OdooBot. Kita melakukan pewarisan (inheritance) bertipe AbstractModel pada model mail.bot guna memotong jalur pesan masuk dan mengalihkannya ke local LLM.
Lokasi file: models/mail_bot.py
class MailBot(models.AbstractModel): _inherit = "mail.bot" def _get_answer(self, record, body, values, command=False): odoobot_partner = self.env.ref("base.partner_root") is_dm = record.channel_type == "chat" and odoobot_partner in record.channel_member_ids.partner_id is_pinged = odoobot_partner.id in values.get("partner_ids", []) if not (is_dm or is_pinged): return False # Bypass commands & tour onboarding if command or body.startswith("/") or body.startswith(":"): return super(MailBot, self)._get_answer(record, body, values, command) if _("start the tour") in body.lower(): return super(MailBot, self)._get_answer(record, body, values, command) try: clean_message = re.sub(r"<[^<]+?>", "", values.get("body", "")).replace("\xa0", " ").strip() if not clean_message: return super(MailBot, self)._get_answer(record, body, values, command) user_context = self._get_session_context(record, clean_message) ai_result = self.env["llama.engine"].generate_response(clean_message, user_context) # Konversi string HTML ke class Markup agar Odoo tidak meng-escape output if ai_result["type"] == "code": return Markup(ai_result["content"]) elif ai_result["type"] == "response": return Markup(self._format_markdown_response(ai_result["content"])) elif ai_result["type"] == "error": return Markup(f'<p style="color: #dc3545;"><strong>Error:</strong> {ai_result["content"]}</p>') except Exception as e: _logger.error(f"Local Llama: Failed to generate response: {e}") return super(MailBot, self)._get_answer(record, body, values, command)
4.2 Pembersihan Tag HTML & Ekstraksi Metadata Sesi Pengguna
Pesan dari Discuss dikirim dalam format HTML terbungkus tag paragraph. Kita membersihkannya sebelum dikirimkan ke model GGUF demi menjaga kualitas inferensi.
def _get_session_context(self, record, current_message): from datetime import date context = { "user_name": self.env.user.name, "user_login": self.env.user.login, "company_name": self.env.company.name, "today": str(date.today()), "history": self._get_chat_history(record, current_message) } return context
4.3 Parser Markdown ke HTML Kustom untuk Mengatasi Pembatasan Mail Sanitizer
Odoo Discuss menggunakan library mail.sanitizer untuk memfilter input teks obrolan demi mencegah celah keamanan Cross-Site Scripting (XSS). Namun, pembersihan ketat ini memotong beberapa sintaksis rendering Markdown mentah yang dihasilkan oleh LLM.
def _format_markdown_response(self, text): if not text: return "" # 1. Konversi karakter HTML sensitif untuk menghentikan XSS escaped_text = html.escape(text) # 2. Format cetak tebal (**teks**) menjadi <strong> escaped_text = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", escaped_text) # 3. Format cetak miring (*teks*) menjadi <em> escaped_text = re.sub(r"\*([^*]+)\*", r"<em>\1</em>", escaped_text) # 4. Format tag kode inline (`kode`) dengan inline CSS styling code_style = "background-color: #f8f9fa; color: #dc3545; padding: 2px 4px; border-radius: 3px; font-family: monospace;" escaped_text = re.sub(r"`([^`]+)`", f'<code style="{code_style}">\\1</code>', escaped_text) # 5. Pecah paragraf (\n\n) dan konversi line break (\n) menjadi tag paragraf <p> paragraphs = escaped_text.split("\n\n") html_paragraphs = [] for p in paragraphs: p_clean = p.strip().replace("\n", "<br/>") if p_clean: html_paragraphs.append(f'<p style="margin-bottom: 8px;">{p_clean}</p>') return "".join(html_paragraphs)
Automated Unit Testing & QA Strategy
Fokus Bab: Menyusun automated tests di Odoo yang membuktikan validitas AST parser dan alur redirect OdooBot tanpa memakan RAM server untuk load model GGUF.
5.1 Teknik Isolasi Model & Mocking Inferensi LLM Lokal
Dalam pipeline Integrasi Kontinu (CI/CD) atau pengujian lokal server Odoo, kita tidak ingin memuat model biner GGUF berukuran gigabyte ke memori RAM/VRAM secara nyata. Mengisi RAM server pengetesan hanya untuk menjalankan unit test adalah pemborosan resource yang besar. Untuk mengatasinya, kita menggunakan teknik Mocking Isolation menggunakan decorator @patch dari library bawaan Python unittest.mock. Kita mem-patch method _call_local_inference pada class LlamaEngine sehingga alur inferensi LLM langsung disimulasikan (mocked) untuk mengembalikan string dummy terformat instan tanpa pernah memanggil model biner C++ native.
from unittest.mock import patch from odoo.tests.common import TransactionCase class TestLlamaChat(TransactionCase): def setUp(self): super(TestLlamaChat, self).setUp() self.llama_engine = self.env["llama.engine"] self.mail_bot = self.env["mail.bot"] self.config_settings_model = self.env["res.config.settings"] @patch("odoo.addons.llama_cpp_ok.models.llama_engine.LlamaEngine._call_local_inference") def test_04_odoobot_chat_redirection(self, mock_inference): mock_inference.return_value = "CODE:result = 50 * 2" channel = self.env["discuss.channel"].create({"name": "Test Chat", "channel_type": "chat"}) ans = self.mail_bot._get_answer(channel, "hitung 50 x 2", {"body": "hitung 50 x 2"}) self.assertIn("100", ans)
5.2 Kasus Uji Unit Test AST Security & Sandbox
Kita merancang pengujian menyeluruh untuk memverifikasi bahwa AST Security Sandbox bekerja secara optimal tanpa celah bypass. Berikut adalah rincian skenario uji terintegrasi:
| Skenario Uji | Deskripsi Kasus & Kode Input | Ekspektasi Hasil |
|---|---|---|
| â Valid ORM Queries | env['res.partner'].search([('name', '=', 'Mitchell Admin')]) | Lolos (Allowed) |
| â Forbidden Imports | import os; result = os.system('ls') | Blocked (ValueError) |
| â Forbidden Dunder Attributes | result = env['res.partner'].__class__.__base__ | Blocked (ValueError) |
| â Dangerous Builtins | result = eval("5 + 5") | Blocked (ValueError) |
| â Protected Model Blacklist | result = env['res.users'].search([]) | Blocked (ValueError) |
| â Non-Existent Model | result = env['res.fiktif'].search([]) | Blocked (ValueError) |
| â Syntax Error Recovery | result = env['res.partner'].search([ | Bypass to Normal Chat |
Lokasi file: tests/test_llama_chat.py
def test_01_ast_security_sandbox(self): # 1. Valid ORM - Harus Lolos valid_code = "result = env['res.partner'].search([('name', '=', 'Mitchell Admin')])" self.assertTrue(self.llama_engine.validate_code_security(valid_code)) # 2. Block Import - Harus Gagal forbidden_import = "import os\nresult = os.system('ls')" with self.assertRaises(ValueError) as context: self.llama_engine.validate_code_security(forbidden_import) self.assertIn("import", str(context.exception)) # 3. Block Dunder - Harus Gagal forbidden_dunder = "result = env['res.partner'].__class__.__base__" with self.assertRaises(ValueError) as context: self.llama_engine.validate_code_security(forbidden_dunder) self.assertIn("Double underscore", str(context.exception))
5.3 Prosedur Eksekusi Automated Test Suite Lintas Platform
Untuk memicu pengetesan unit test modular Odoo secara otomatis, jalankan CLI runner sesuai dengan shell console masing-masing sistem operasi target Anda:
Linux & macOS (Bash Shell):
conda run -n odoo-18 python [path_to_odoo]/odoo-bin -c [path_to_odoo]/odoo-apps-nolog.conf -i llama_cpp_ok --test-enable --test-tags=llama_cpp_ok --stop-after-init
Windows (PowerShell Console):
conda run -n odoo-18 python "[path_to_odoo]\odoo-bin" -c "[path_to_odoo]\odoo-apps-nolog.conf" -i llama_cpp_ok --test-enable --test-tags=llama_cpp_ok --stop-after-init
--stop-after-init memastikan server Odoo akan berhenti secara otomatis setelah seluruh test suite selesai dievaluasi.
Layanan Training & Dukungan Donasi
Apakah Anda memerlukan bimbingan teknis mendalam mengenai implementasi AI lokal, optimasi model offline, custom workflow sandbox, atau arsitektur modular di Odoo? Kami membuka pendaftaran untuk Sesi Training Online Privat & Corporate. Anda juga sangat disarankan untuk membeli addon resmi ini di Odoo App Store sebagai bentuk donasi dukungan riset berkelanjutan serta penyediaan panduan gratis berkualitas bagi komunitas pengembang Odoo.
© 2026 Developer Training Module. Created with Pride for Odoo 18 & Llama.cpp Offline Training.
Odoo Proprietary License v1.0 This software and associated files (the "Software") may only be used (executed, modified, executed after modifications) if you have purchased a valid license from the authors, typically via Odoo Apps, or if you have received a written agreement from the authors of the Software (see the COPYRIGHT file). You may develop Odoo modules that use the Software as a library (typically by depending on it, importing it and using its resources), but without copying any source code or material from the Software. You may distribute those modules under the license of your choice, provided that this license is compatible with the terms of the Odoo Proprietary License (For example: LGPL, MIT, or proprietary licenses similar to this one). It is forbidden to publish, distribute, sublicense, or sell copies of the Software or modified copies of the Software. The above copyright notice and this permission notice must be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Please log in to comment on this module