@ -0,0 +1,120 @@ |
|||
# Ora-Ai Base Odoo 17 |
|||
|
|||
[](https://www.odoo.com) |
|||
[](https://opensource.org/licenses/MIT) |
|||
|
|||
## Overview |
|||
|
|||
This module facilitates order placement via ora AI voice assistance, |
|||
seamlessly integrating with the sale order system to enhance customer service and streamline operations. |
|||
|
|||
## Features |
|||
|
|||
- ⏱️**Reducing Order Time.** |
|||
- 🎙️**Replacing Employees with Voice Assistance.** |
|||
- 🕒**24/7 Availability.** |
|||
- 🛒**Automated Sale Order Creation.** |
|||
- 🌐**Starts in English, then auto-detects and switches |
|||
to your default language set in the voice assistant.** |
|||
- 🧅**Addon Product Options and Variants.** |
|||
- 📢**Promotional Content Integration.** |
|||
|
|||
## Screenshots |
|||
|
|||
Here are some glimpses of ORA AI: |
|||
|
|||
### VAPI AI Interface |
|||
|
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/aa.png" alt="Feature 1" width="500" style="border: none;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
|
|||
### ORA AI Base Interface |
|||
|
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/ora_1.png" alt="Feature 1" width="500" style="border: none;border-radius: 5px;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
|
|||
Configure the Assistants. |
|||
|
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/ora_7.png" alt="Feature 1" width="500" style="border: none;border-radius: 5px;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
|
|||
Run tests for the assistants. |
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/ora_8.png" alt="Feature 1" width="500" style="border: none;border-radius: 5px;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
|
|||
## Prerequisites |
|||
|
|||
Before you begin, ensure you have the following installed: |
|||
|
|||
- An active Odoo Community/Enterprise Edition instance (local or hosted) |
|||
|
|||
## Configuration |
|||
|
|||
- Once installed, users will be able to add the API details for VAPI AI. |
|||
Company |
|||
|
|||
## Contributing |
|||
|
|||
We welcome contributions! To get started: |
|||
|
|||
1. Fork the repository. |
|||
|
|||
2. Create a new branch: |
|||
``` |
|||
git checkout -b feature/your-feature-name |
|||
``` |
|||
3. Make changes and commit: |
|||
``` |
|||
git commit -m "Add your message here" |
|||
``` |
|||
4. Push your changes: |
|||
``` |
|||
git push origin feature/your-feature-name |
|||
``` |
|||
5. Create a Pull Request on GitHub. |
|||
|
|||
--- |
|||
|
|||
- Submit a pull request with a clear description of your changes. |
|||
|
|||
## License |
|||
|
|||
This project is licensed under the AGPL-3. Feel free to use, modify, and distribute it as needed. |
|||
|
|||
Company |
|||
------- |
|||
* `Cybrosys Techno Solutions <https://cybrosys.com/>` |
|||
|
|||
## Contact |
|||
|
|||
* Mail Contact : odoo@cybrosys.com |
|||
* Website : https://cybrosys.com |
|||
|
|||
Maintainer |
|||
========== |
|||
 |
|||
https://cybrosys.com |
|||
|
|||
|
|||
This module is maintained by Cybrosys Technologies. |
|||
For support and more information, please visit https://www.cybrosys.com |
|||
@ -0,0 +1,24 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from . import controllers |
|||
from . import models |
|||
from . import wizard |
|||
@ -0,0 +1,63 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
{ |
|||
'name': "Ora-Ai Base", |
|||
'version': '17.0.1.0.0', |
|||
'category': 'eCommerce', |
|||
'summary': 'Ora-Ai base module helps to configure the voice assistant.', |
|||
'description': 'Ora-AI base module provides core configurations and ' |
|||
'services necessary to integrate and manage a voice ' |
|||
'assistant within the Odoo environment.', |
|||
'author': 'Cybrosys Techno Solutions', |
|||
'company': 'Cybrosys Techno Solutions', |
|||
'maintainer': 'Cybrosys Techno Solutions', |
|||
'website': "https://www.cybrosys.com", |
|||
'depends': ['base', 'bus', 'mail', 'website_sale'], |
|||
'data': [ |
|||
'security/ir.model.access.csv', |
|||
'data/client_action.xml', |
|||
'data/provider_model_data.xml', |
|||
'data/transcriber_model_data.xml', |
|||
'data/ora_langauage_data.xml', |
|||
'views/ora_ai_views.xml', |
|||
'views/ora_file_view.xml', |
|||
'wizard/res_config_settings_views.xml', |
|||
'views/vapi_menus.xml', |
|||
], |
|||
'assets': { |
|||
'web.assets_backend': [ |
|||
'ora_ai_base/static/src/js/ora_voice_data.js', |
|||
'ora_ai_base/static/src/js/ora_ai.js', |
|||
'ora_ai_base/static/src/xml/ora_ai_base_templates.xml', |
|||
'ora_ai_base/static/src/xml/ora_ai_base_voice_data_templates.xml', |
|||
'ora_ai_base/static/src/scss/ora_ai.scss', |
|||
'ora_ai_base/static/src/scss/style.scss', |
|||
'https://cdn.jsdelivr.net/npm/@vapi-ai/web@2.3.8/dist/vapi.min.js' |
|||
], |
|||
}, |
|||
'external_dependencies': {"python": ["zeep"]}, |
|||
'images': ['static/description/banner.jpg'], |
|||
'license': 'AGPL-3', |
|||
'auto_install': False, |
|||
'installable': True, |
|||
'application': True, |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from . import ora_ai_base |
|||
@ -0,0 +1,80 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from odoo import Command, http |
|||
from odoo.http import request |
|||
|
|||
class VapiAssistant(http.Controller): |
|||
@http.route('/vapi_voice_assistant/details', type='json', |
|||
auth='public', csrf=False, methods=['POST']) |
|||
def final_details(self): |
|||
"""Processes final order details received from the voice assistant |
|||
and creates a Sale Order in Odoo.""" |
|||
data = request.httprequest.get_json() |
|||
call = data['message']['call'] |
|||
call_type = call.get('type') |
|||
tool_call = data['message']['toolCalls'][0] |
|||
arguments = tool_call['function']['arguments'] |
|||
details = arguments['OrderDetails']['Products'] |
|||
pr_list = [] |
|||
customer = "" |
|||
for rec in details: |
|||
pr_list.append( |
|||
{"customer": rec['Customer'], "product": rec['Product'], |
|||
"id": rec['productId'], "qty": rec['Quantity'], |
|||
"variant": rec['Variant']}) |
|||
customer = rec['Customer'] |
|||
channel = "vapi_voice_channel" |
|||
message = {"value": pr_list, "channel": channel} |
|||
request.env["bus.bus"]._sendone(channel, "notification", message) |
|||
if call_type == 'inboundPhoneCall': |
|||
number = call['customer']['number'] |
|||
cust = request.env['res.partner'].sudo().search( |
|||
[('phone', '=', number)]) |
|||
if not cust: |
|||
cust = request.env['res.partner'].sudo().create({ |
|||
'name': customer, |
|||
'phone': number, |
|||
}) |
|||
sales = request.env['sale.order'].sudo().create({ |
|||
'partner_id': cust.id, |
|||
'order_line': [ |
|||
Command.create({ |
|||
'product_id': recs['id'], |
|||
'product_uom_qty': recs["qty"], |
|||
}) for recs in pr_list] |
|||
}) |
|||
sales.action_confirm() |
|||
|
|||
@http.route('/vapi_voice_assistant/status', type='json', |
|||
auth='public', csrf=False, methods=['POST']) |
|||
def get_status(self): |
|||
"""Receives status updates from the voice assistant""" |
|||
data = request.httprequest.get_json() |
|||
channel = "inbound_call_channel" |
|||
message = {"value": data, "channel": channel} |
|||
request.env["bus.bus"]._sendone(channel, "notification", message) |
|||
|
|||
@http.route('/vapi_voice_assistant/language_details', type='json', |
|||
auth='public', csrf=False, methods=['POST']) |
|||
def get_language_details(self): |
|||
"""Returns language-specific configuration.""" |
|||
request.httprequest.get_json() |
|||
@ -0,0 +1,8 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<odoo> |
|||
<!-- Client action for launching the Voice Assistant interface in Odoo.--> |
|||
<record id="voice_assistant_client_action" model="ir.actions.client"> |
|||
<field name="name">Voice Assistant</field> |
|||
<field name="tag">action_voice_assistant</field> |
|||
</record> |
|||
</odoo> |
|||
@ -0,0 +1,82 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<odoo noupdate="1"> |
|||
<!-- Supported languages--> |
|||
<record id="lang_en" model="ora.language"> |
|||
<field name="name">English</field> |
|||
<field name="code">en</field> |
|||
<field name="first_msg">I am ready to place an order with you.</field> |
|||
<field name="voice">qgj3VahzWaAK300v6H27</field> |
|||
</record> |
|||
<record id="lang_es" model="ora.language"> |
|||
<field name="name">Spanish</field> |
|||
<field name="code">es</field> |
|||
<field name="first_msg">Estoy listo para hacer un pedido con usted. |
|||
</field> |
|||
<field name="voice">AzEJh3iQsk0op7UzS49R</field> |
|||
</record> |
|||
<record id="lang_fr" model="ora.language"> |
|||
<field name="name">French</field> |
|||
<field name="code">fr</field> |
|||
<field name="first_msg">Je suis prêt à passer une commande chez vous. |
|||
</field> |
|||
<field name="voice">kVQrtkfBI5wqyVK1NLZa</field> |
|||
</record> |
|||
<record id="lang_hi" model="ora.language"> |
|||
<field name="name">Hindi</field> |
|||
<field name="code">hi</field> |
|||
<field name="first_msg">आपको ऑर्डर देने के लिए तैयार हूं</field> |
|||
<field name="voice">vj2Y0u1NaT1ayH6VYkYu</field> |
|||
</record> |
|||
<record id="lang_hu" model="ora.language"> |
|||
<field name="name">Hungarian</field> |
|||
<field name="code">hu</field> |
|||
<field name="first_msg">Készen állok rendelést leadni Öntől.</field> |
|||
<field name="voice">JCNhnuny412iVIvPSTD4</field> |
|||
</record> |
|||
<record id="lang_id" model="ora.language"> |
|||
<field name="name">Indonesian</field> |
|||
<field name="code">id</field> |
|||
</record> |
|||
<record id="lang_it" model="ora.language"> |
|||
<field name="name">Italian</field> |
|||
<field name="code">it</field> |
|||
</record> |
|||
<record id="lang_ja" model="ora.language"> |
|||
<field name="name">Japanese</field> |
|||
<field name="code">ja</field> |
|||
</record> |
|||
<record id="lang_ko" model="ora.language"> |
|||
<field name="name">Korean</field> |
|||
<field name="code">ko</field> |
|||
</record> |
|||
<record id="lang_lt" model="ora.language"> |
|||
<field name="name">Lithuanian</field> |
|||
<field name="code">lt</field> |
|||
</record> |
|||
<record id="lang_lv" model="ora.language"> |
|||
<field name="name">Latvian</field> |
|||
<field name="code">lv</field> |
|||
</record> |
|||
<record id="lang_ms" model="ora.language"> |
|||
<field name="name">Malay</field> |
|||
<field name="code">ms</field> |
|||
</record> |
|||
<record id="lang_nl" model="ora.language"> |
|||
<field name="name">Dutch</field> |
|||
<field name="code">nl</field> |
|||
<field name="first_msg">Ik ben klaar om een bestelling bij u te plaatsen.</field> |
|||
<field name="voice">Lz2ReR8kYZjz4dhllNDu</field> |
|||
</record> |
|||
<record id="lang_th" model="ora.language"> |
|||
<field name="name">Thai</field> |
|||
<field name="code">th</field> |
|||
</record> |
|||
<record id="lang_vi" model="ora.language"> |
|||
<field name="name">Vietnamese</field> |
|||
<field name="code">vi</field> |
|||
</record> |
|||
<record id="lang_zh" model="ora.language"> |
|||
<field name="name">Chinese</field> |
|||
<field name="code">zh</field> |
|||
</record> |
|||
</odoo> |
|||
@ -0,0 +1,121 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<odoo noupdate="1"> |
|||
<!-- AI model configurations.--> |
|||
<record id="GPT_4o_Cluster_openai" model="provider.model"> |
|||
<field name="name">GPT 4o Cluster</field> |
|||
<field name="key">gpt-4o</field> |
|||
<field name="provider">openai</field> |
|||
</record> |
|||
<record id="GPT_4o_Mini_Cluster_openai" model="provider.model"> |
|||
<field name="name">GPT 4o Mini Cluster</field> |
|||
<field name="key">gpt-4o-mini</field> |
|||
<field name="provider">openai</field> |
|||
</record> |
|||
<record id="GPT_3_5_Turbo_Cluster_openai" model="provider.model"> |
|||
<field name="name">GPT 3.5 Turbo Cluster</field> |
|||
<field name="key">gpt-3.5-turbo</field> |
|||
<field name="provider">openai</field> |
|||
</record> |
|||
<record id="GPT_4_Turbo_Cluster_openai" model="provider.model"> |
|||
<field name="name">GPT 4 Turbo Cluster</field> |
|||
<field name="key">gpt-4</field> |
|||
<field name="provider">openai</field> |
|||
</record> |
|||
<record id="gpt-3_5-turbo_together-ai" model="provider.model"> |
|||
<field name="name">gpt-3.5-turbo</field> |
|||
<field name="key">gpt-3.5-turbo</field> |
|||
<field name="provider">together-ai</field> |
|||
</record> |
|||
<record id="gpt-3_5-turbo_anyscale" model="provider.model"> |
|||
<field name="name">gpt-3.5-turbo</field> |
|||
<field name="key">gpt-3.5-turbo</field> |
|||
<field name="provider">anyscale</field> |
|||
</record> |
|||
<record id="gpt-3_5-turbo_openrouter" model="provider.model"> |
|||
<field name="name">gpt-3.5-turbo</field> |
|||
<field name="key">gpt-3.5-turbo</field> |
|||
<field name="provider">openrouter</field> |
|||
</record> |
|||
<record id="gpt-3_5-turbo_perplexity-ai" model="provider.model"> |
|||
<field name="name">gpt-3.5-turbo</field> |
|||
<field name="key">gpt-3.5-turbo</field> |
|||
<field name="provider">perplexity-ai</field> |
|||
</record> |
|||
<record id="gpt-3_5-turbo_deepinfra" model="provider.model"> |
|||
<field name="name">gpt-3.5-turbo</field> |
|||
<field name="key">gpt-3.5-turbo</field> |
|||
<field name="provider">deepinfra</field> |
|||
</record> |
|||
<record id="llama-3_1-405b-reasoning_groq" model="provider.model"> |
|||
<field name="name">llama-3.1-405b-reasoning</field> |
|||
<field name="key">llama-3.1-405b-reasoning</field> |
|||
<field name="provider">groq</field> |
|||
</record> |
|||
<record id="llama-3_1-70b-versatile_groq" model="provider.model"> |
|||
<field name="name">llama-3.1-70b-versatile</field> |
|||
<field name="key">llama-3.1-70b-versatile</field> |
|||
<field name="provider">groq</field> |
|||
</record> |
|||
<record id="llama-3_1-8b-instant_groq" model="provider.model"> |
|||
<field name="name">llama-3.1-8b-instant</field> |
|||
<field name="key">llama-3.1-8b-instant</field> |
|||
<field name="provider">groq</field> |
|||
</record> |
|||
<record id="mixtral-8x7b-32768_groq" model="provider.model"> |
|||
<field name="name">mixtral-8x7b-32768</field> |
|||
<field name="key">mixtral-8x7b-32768</field> |
|||
<field name="provider">groq</field> |
|||
</record> |
|||
<record id="llama3-8b-8192_groq" model="provider.model"> |
|||
<field name="name">llama3-8b-8192</field> |
|||
<field name="key">llama3-8b-8192</field> |
|||
<field name="provider">groq</field> |
|||
</record> |
|||
<record id="llama3-70b-8192_groq" model="provider.model"> |
|||
<field name="name">llama3-70b-8192</field> |
|||
<field name="key">llama3-70b-8192</field> |
|||
<field name="provider">groq</field> |
|||
</record> |
|||
<record id="llama3-groq-8b-8192-tool-use-preview_groq" |
|||
model="provider.model"> |
|||
<field name="name">llama3-groq-8b-8192-tool-use-preview</field> |
|||
<field name="key">llama3-groq-8b-8192-tool-use-preview</field> |
|||
<field name="provider">groq</field> |
|||
</record> |
|||
<record id="llama3-groq-70b-8192-tool-use-preview_groq" |
|||
model="provider.model"> |
|||
<field name="name">llama3-groq-70b-8192-tool-use-preview</field> |
|||
<field name="key">llama3-groq-70b-8192-tool-use-preview</field> |
|||
<field name="provider">groq</field> |
|||
</record> |
|||
<record id="gemma-7b-it_groq" model="provider.model"> |
|||
<field name="name">gemma-7b-it</field> |
|||
<field name="key">gemma-7b-it</field> |
|||
<field name="provider">groq</field> |
|||
</record> |
|||
<record id="gemma2-9b-it_groq" model="provider.model"> |
|||
<field name="name">gemma2-9b-it</field> |
|||
<field name="key">gemma2-9b-it</field> |
|||
<field name="provider">groq</field> |
|||
</record> |
|||
<record id="claude-3-opus-20240229_anthropic" model="provider.model"> |
|||
<field name="name">claude-3-opus-20240229</field> |
|||
<field name="key">claude-3-opus-20240229</field> |
|||
<field name="provider">anthropic</field> |
|||
</record> |
|||
<record id="claude-3-sonnet-20240229_anthropic" model="provider.model"> |
|||
<field name="name">claude-3-sonnet-20240229</field> |
|||
<field name="key">claude-3-sonnet-20240229</field> |
|||
<field name="provider">anthropic</field> |
|||
</record> |
|||
<record id="claude-3-haiku-20240307_anthropic" model="provider.model"> |
|||
<field name="name">claude-3-haiku-20240307</field> |
|||
<field name="key">claude-3-haiku-20240307</field> |
|||
<field name="provider">anthropic</field> |
|||
</record> |
|||
<record id="claude-3-5-sonnet-20240620_anthropic" model="provider.model"> |
|||
<field name="name">claude-3-5-sonnet-20240620</field> |
|||
<field name="key">claude-3-5-sonnet-20240620</field> |
|||
<field name="provider">anthropic</field> |
|||
</record> |
|||
</odoo> |
|||
@ -0,0 +1,74 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<odoo noupdate="1"> |
|||
<!--Transcribe spoken input into text.--> |
|||
<record id="nova-2_deepgram" model="transcriber.model"> |
|||
<field name="name">nova-2</field> |
|||
<field name="key">nova-2</field> |
|||
<field name="provider">deepgram</field> |
|||
</record> |
|||
<record id="nova-2-general_deepgram" model="transcriber.model"> |
|||
<field name="name">nova-2-general</field> |
|||
<field name="key">nova-2-general</field> |
|||
<field name="provider">deepgram</field> |
|||
</record> |
|||
<record id="nova-2-meeting_deepgram" model="transcriber.model"> |
|||
<field name="name">nova-2-meeting</field> |
|||
<field name="key">nova-2-meeting</field> |
|||
<field name="provider">deepgram</field> |
|||
</record> |
|||
<record id="nova-2-phonecall_deepgram" model="transcriber.model"> |
|||
<field name="name">nova-2-phonecall</field> |
|||
<field name="key">nova-2-phonecall</field> |
|||
<field name="provider">deepgram</field> |
|||
</record> |
|||
<record id="nova-2-finance_deepgram" model="transcriber.model"> |
|||
<field name="name">nova-2-finance</field> |
|||
<field name="key">nova-2-finance</field> |
|||
<field name="provider">deepgram</field> |
|||
</record> |
|||
<record id="nova-2-conversationalai_deepgram" model="transcriber.model"> |
|||
<field name="name">nova-2-conversationalai</field> |
|||
<field name="key">nova-2-conversationalai</field> |
|||
<field name="provider">deepgram</field> |
|||
</record> |
|||
<record id="nova-2-voicemail_deepgram" model="transcriber.model"> |
|||
<field name="name">nova-2-voicemail</field> |
|||
<field name="key">nova-2-voicemail</field> |
|||
<field name="provider">deepgram</field> |
|||
</record> |
|||
<record id="nova-2-video_deepgram" model="transcriber.model"> |
|||
<field name="name">nova-2-video</field> |
|||
<field name="key">nova-2-video</field> |
|||
<field name="provider">deepgram</field> |
|||
</record> |
|||
<record id="nova-2-medical_deepgram" model="transcriber.model"> |
|||
<field name="name">nova-2-medical</field> |
|||
<field name="key">nova-2-medical</field> |
|||
<field name="provider">deepgram</field> |
|||
</record> |
|||
<record id="nova-2-drivethru_deepgram" model="transcriber.model"> |
|||
<field name="name">nova-2-drivethru</field> |
|||
<field name="key">nova-2-drivethru</field> |
|||
<field name="provider">deepgram</field> |
|||
</record> |
|||
<record id="nova-2-automotive_deepgram" model="transcriber.model"> |
|||
<field name="name">nova-2-automotive</field> |
|||
<field name="key">nova-2-automotive</field> |
|||
<field name="provider">deepgram</field> |
|||
</record> |
|||
<record id="whisper_talkscriber" model="transcriber.model"> |
|||
<field name="name">Whisper</field> |
|||
<field name="key">whisper</field> |
|||
<field name="provider">talkscriber</field> |
|||
</record> |
|||
<record id="fast_gladia" model="transcriber.model"> |
|||
<field name="name">Fast</field> |
|||
<field name="key">fast</field> |
|||
<field name="provider">gladia</field> |
|||
</record> |
|||
<record id="accurate_gladia" model="transcriber.model"> |
|||
<field name="name">Accurate</field> |
|||
<field name="key">accurate</field> |
|||
<field name="provider">gladia</field> |
|||
</record> |
|||
</odoo> |
|||
@ -0,0 +1,7 @@ |
|||
## Module <ora_ai_base> |
|||
|
|||
#### 19.11.2025 |
|||
#### Version 17.0.1.0.0 |
|||
#### ADD |
|||
|
|||
- Initial commit for Ora-Ai Base |
|||
@ -0,0 +1,27 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from . import ora_ai |
|||
from . import ora_file |
|||
from . import ora_language |
|||
from . import provider_model |
|||
from . import transcriber_model |
|||
|
|||
@ -0,0 +1,668 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from googletrans import Translator |
|||
from odoo import api, fields, models |
|||
import requests |
|||
from odoo.exceptions import AccessError, ValidationError |
|||
|
|||
PROVIDER = [ |
|||
('openai', 'openai'), |
|||
('together-ai', 'Together-AI'), |
|||
('anyscale', 'AnyScale'), |
|||
('openrouter', 'OpenRouter'), |
|||
('perplexity-ai', 'Perplexity-AI'), |
|||
('deepinfra', 'DeepInfra'), |
|||
('groq', 'Groq'), |
|||
('anthropic', 'Anthropic') |
|||
] |
|||
TRANSCRIBER_PROVIDER = [('deepgram', 'Deepgram'), |
|||
('talkscriber', 'Talkscriber'), |
|||
('gladia', 'Gladiya')] |
|||
STATE = [('draft', 'Draft'), |
|||
('done', 'Done')] |
|||
|
|||
|
|||
class OraAi(models.Model): |
|||
"""Model for the order assistant.""" |
|||
_name = "ora.ai" |
|||
_inherit = "mail.thread" |
|||
_description = "Order Assistant" |
|||
|
|||
name = fields.Char(string="Name", required=True, |
|||
help="Name of the AI assistant configuration.") |
|||
id_assistant = fields.Char(string="Assistant id", readonly=True, |
|||
copy=False, help="Unique identifier of the" |
|||
"assistant created via " |
|||
"external API.") |
|||
first_message = fields.Text(string="First Message", |
|||
compute="_compute_first_message", |
|||
help="The first message that the assistant" |
|||
" will say.", ) |
|||
provider = fields.Selection(string="Provider", selection=PROVIDER, |
|||
default="openai", required=True, |
|||
help="Select the LLM provider for generating" |
|||
" responses (e.g., OpenAI, Anthropic, " |
|||
"Groq).") |
|||
provider_model_id = fields.Many2one('provider.model', |
|||
string="Model", |
|||
domain="[('id', 'in',provider_model_ids)]", |
|||
required=True, |
|||
help="Select the specific AI model" |
|||
"offered by the chosen provider.") |
|||
provider_model_ids = fields.Many2many('provider.model', |
|||
compute='_compute_provider_models', |
|||
help="Filtered list of models " |
|||
"available based on the " |
|||
"selected provider.") |
|||
transcriber_provider = fields.Selection(selection=TRANSCRIBER_PROVIDER, |
|||
string="Transcriber Provider", |
|||
default='deepgram', required=True, |
|||
help="Speech-to-text service " |
|||
"provider for transcribing " |
|||
"voice input.") |
|||
transcriber_model_id = fields.Many2one('transcriber.model', |
|||
string="Transcriber Model", |
|||
required=True, |
|||
domain="[('id', 'in', transcriber_model_ids)]", |
|||
help="Choose the transcription " |
|||
"model best suited for the " |
|||
"conversation context.") |
|||
transcriber_model_ids = fields.Many2many('transcriber.model', |
|||
compute='_compute_transcriber_models', |
|||
help="List of available " |
|||
"transcription models " |
|||
"filtered by selected " |
|||
"provider.") |
|||
language_id = fields.Many2one('ora.language', |
|||
string="Language", |
|||
help="Select the default language " |
|||
"used by the assistant.") |
|||
is_lang_switch = fields.Boolean(string="Multi-Language", |
|||
help="Enable support for multiple " |
|||
"languages. Assistant will switch" |
|||
" languages if needed.") |
|||
language_ids = fields.Many2many('ora.language', |
|||
string="Languages", |
|||
help="List of available languages for " |
|||
"dynamic language switching during " |
|||
"the session.") |
|||
state = fields.Selection(selection=STATE, tracking=True, |
|||
default="draft", copy=False, |
|||
help="Current status of the assistant" |
|||
" configuration") |
|||
date = fields.Date(string="Date", |
|||
help="Date when the assistant was created or" |
|||
" activated.") |
|||
prompt = fields.Text(string="Contents", |
|||
compute="_compute_prompt", |
|||
readonly=True, |
|||
help="The Contents can be used to configure the" |
|||
" context, role, personality, instructions " |
|||
"and so on for the assistant.", ) |
|||
end_call_phrases = fields.Text(string="End Call Phrases", |
|||
default="goodbye", |
|||
help="Enter phrases, separated by commas, " |
|||
"that will trigger the Assistant to " |
|||
"end the call when spoken.") |
|||
file_ids = fields.Many2many('ora.file', |
|||
string="Knowledge Base", |
|||
help="Knowledge Base is a collection of" |
|||
" custom documents that contain " |
|||
"information on specific topics or" |
|||
" domains.") |
|||
function_description = fields.Char(string="Function", |
|||
compute="_compute_function_description", |
|||
help="Generated JSON-based function") |
|||
language_function_description = fields.Char( |
|||
string="language function description", |
|||
compute="_compute_language_function_description", |
|||
help="Generated function spec for language preference " |
|||
"handling used by the assistant.") |
|||
|
|||
def _translate_text(self, text, target_lang): |
|||
"""Translates the given text into the specified language using |
|||
Googletrans.""" |
|||
translator = Translator() |
|||
translated = translator.translate(text, dest=target_lang) |
|||
return translated.text |
|||
|
|||
@api.depends('provider') |
|||
def _compute_provider_models(self): |
|||
"""Populate the list of available provider models based on the |
|||
selected provider.""" |
|||
for rec in self: |
|||
provider_model = rec.provider_model_id.search([]) |
|||
for provider in PROVIDER: |
|||
if rec.provider == provider[0]: |
|||
filtered_model = provider_model.filtered( |
|||
lambda l: l.provider == provider[0]) |
|||
rec.provider_model_ids = [fields.Command.link(res.id) |
|||
for res in filtered_model] |
|||
if not rec.provider: |
|||
rec.provider_model_ids = [fields.Command.link(res.id) |
|||
for res in provider_model] |
|||
|
|||
@api.depends('transcriber_provider') |
|||
def _compute_transcriber_models(self): |
|||
"""Assign available transcriber models based on the selected |
|||
transcriber provider""" |
|||
for rec in self: |
|||
provider_model = rec.transcriber_model_id.search([]) |
|||
for provider in TRANSCRIBER_PROVIDER: |
|||
if rec.transcriber_provider == provider[0]: |
|||
filtered_model = provider_model.filtered( |
|||
lambda l: l.provider == provider[0]) |
|||
rec.transcriber_model_ids = [fields.Command.link(res.id) |
|||
for res in filtered_model] |
|||
if not rec.transcriber_provider: |
|||
rec.transcriber_model_ids = [fields.Command.link(res.id) |
|||
for res in provider_model] |
|||
|
|||
@api.depends('language_ids') |
|||
def _compute_language_function_description(self): |
|||
"""Computes a formatted description of available languages |
|||
for the assistant.""" |
|||
for rec in self: |
|||
language_function_description = "" |
|||
if rec.language_ids and rec.is_lang_switch: |
|||
for lang in rec.language_ids: |
|||
language_function_description += (f"{lang.name} : " |
|||
f"{lang.code}, \n") |
|||
rec.language_function_description = ( |
|||
language_function_description) |
|||
else: |
|||
rec.language_function_description = "" |
|||
|
|||
def _compute_prompt(self): |
|||
"""Computes a detailed prompt for the voice assistant based on |
|||
the current product catalog and availability.""" |
|||
prompt = ( |
|||
f"You are the voice assistant of the 'My Company (San Francisco)' " |
|||
f"Restaurant, responsible for taking customer orders. " |
|||
f"Your primary task is to carefully listen to and process the " |
|||
f"customer's order details.\n" |
|||
f"Must ask for Customer Name.\n" |
|||
f"If a customer asks for product details, first explain all " |
|||
f"the details by category. Then, mention the product name, " |
|||
f"followed by its price, in that order.\n") |
|||
out_of_stock_prompt = "." |
|||
out_of_stock_prompt_2 = "." |
|||
cate_ids = self.env['product.public.category'].search([]) |
|||
for rec in cate_ids: |
|||
prompt += f"\n{rec.display_name}\n" |
|||
products = self.env['product.template'].search( |
|||
[('public_categ_ids', 'in', rec.id), |
|||
('is_published', '=', True)]) |
|||
for record in products: |
|||
prompt += f" • {record.name}, ${record.list_price}\n" |
|||
optional_products = record.optional_product_ids.mapped('name') |
|||
variants = record.product_variant_ids |
|||
if (record.detailed_type == 'product' and |
|||
not record.allow_out_of_stock_order): |
|||
prompt += (f" -quantity available:" |
|||
f"{int(record.qty_available)}\n") |
|||
out_of_stock_prompt = (", don't say the available quantity" |
|||
" even if it is out of stock, say " |
|||
"only when it asked.") |
|||
out_of_stock_prompt_2 = (" ,if the quantity is greater than" |
|||
" available quantity just say the" |
|||
" available quantity in the stock." |
|||
" ") |
|||
if len(variants) > 1: |
|||
prompt += f" - {len(variants)} variants\n" |
|||
for variant in variants: |
|||
attribute_values = ( |
|||
variant.product_template_variant_value_ids.mapped( |
|||
'name')) |
|||
variant_details = ", ".join(attribute_values) |
|||
prompt += (f" * {variant_details}, " |
|||
f"${variant.lst_price}\n") |
|||
if len(optional_products) > 0: |
|||
prompt += (f" - {len(optional_products)} " |
|||
f"Optional Products\n") |
|||
for opt_product in optional_products: |
|||
prompt += f" • {opt_product} \n" |
|||
none_categ_products = self.env['product.template'].search( |
|||
[("public_categ_ids", "=", False), ('is_published', '=', True)]) |
|||
prompt += f"\n None category \n" |
|||
for rec in none_categ_products: |
|||
prompt += f" • {rec.name}, ${rec.list_price} \n" |
|||
prompt += ( |
|||
f"\n When a customer interacts with you through a voice command" |
|||
f" to place an order, \n you should proceed by listing out all " |
|||
f"product options available without excluding any mentioned" |
|||
f" products {out_of_stock_prompt}\n" |
|||
f"if the product has variants ask for which variant do you need.\n" |
|||
f"After customer select a product the ask for How many Quantity" |
|||
f" do you need{out_of_stock_prompt_2}\n" |
|||
f"if the product has optional products ask do you want to add ? \n" |
|||
f"Before confirming the order, ask Would you like to add anything" |
|||
f" else to your order before confirming? \n" |
|||
f"Once the customer finalizes their order and informs you of their" |
|||
f" selection, ask, 'May I confirm the order?' If they respond " |
|||
f"affirmatively, kindly repeat back the order details " |
|||
f"for confirmation.\n Keep all your responses short and simple." |
|||
f" Use casual language, phrases like Umm..., Well..., and I " |
|||
f"mean are preferred.\n If customer order confirmed. say thankyou" |
|||
f" for ordering and goodbye.\n This is a voice conversation, " |
|||
f"so keep your responses short, like in a real conversation. " |
|||
f"Don't ramble for too long") |
|||
self.prompt = prompt |
|||
|
|||
def _compute_function_description(self): |
|||
"""Computes a descriptive mapping of product names, |
|||
variants, and their IDs.""" |
|||
function_description = "" |
|||
products = self.env['product.product'].search([]) |
|||
for product in products: |
|||
variant_values = ", ".join( |
|||
product.product_template_attribute_value_ids.mapped('name')) |
|||
function_description += (f"{product.name} {variant_values} :" |
|||
f" {product.id}, \n") |
|||
self.function_description = function_description |
|||
|
|||
def _compute_first_message(self): |
|||
"""Computes the initial greeting message for the voice assistant.""" |
|||
for rec in self: |
|||
languages = rec.language_ids.mapped('name') |
|||
result = ', '.join(languages) |
|||
if rec.language_ids and rec.is_lang_switch: |
|||
rec.first_message = (f"Hey iam your {self.name}, Which " |
|||
f"language would you like to choose?" |
|||
f" We have several languages available" |
|||
f" such as {result}. Which Language " |
|||
f"would you prefer?") |
|||
else: |
|||
rec.first_message = (f"Hey iam {self.name}, How can i " |
|||
f"help you today?") |
|||
|
|||
def action_create_assistant(self): |
|||
"""Creates a voice assistant instance using the Vapi.ai API based |
|||
on the assistant configuration.""" |
|||
base_url = self.env['ir.config_parameter'].sudo().get_param( |
|||
'web.base.url') |
|||
bearer = self.env['ir.config_parameter'].sudo().get_param( |
|||
'ora_ai_base.vapi_private_api_key') |
|||
protocol = "https" if base_url.startswith("https") else "http" |
|||
if protocol == 'http': |
|||
raise AccessError("URL Must be HTTPS") |
|||
if protocol == 'https': |
|||
url = "https://api.vapi.ai/assistant" |
|||
if self.is_lang_switch and self.language_ids: |
|||
language = "en" |
|||
voice = "qgj3VahzWaAK300v6H27" |
|||
first_message = self.first_message |
|||
else: |
|||
language = self.language_id.code |
|||
voice = self.language_id.voice |
|||
first_message = self._translate_text(self.first_message, |
|||
language) |
|||
payload = { |
|||
"transcriber": { |
|||
"provider": self.transcriber_provider, |
|||
"model": self.transcriber_model_id.key, |
|||
"language": language, |
|||
"smartFormat": True |
|||
}, |
|||
"voice": { |
|||
"voiceId": voice, |
|||
"provider": "11labs", |
|||
}, |
|||
"model": { |
|||
"provider": self.provider, |
|||
"model": self.provider_model_id.key, |
|||
"knowledgeBase": { |
|||
"provider": "canonical", |
|||
"fileIds": self.file_ids.mapped('id_file') |
|||
}, |
|||
"messages": [ |
|||
{ |
|||
"role": "system", |
|||
"content": self.prompt |
|||
} |
|||
], |
|||
"tools": [ |
|||
{ |
|||
"type": "function", |
|||
"async": True, |
|||
"function": { |
|||
"name": "GetFinalOrderDetails", |
|||
"description": f"This function is designed to" |
|||
f"retrieve order details from " |
|||
f"a voice assistant, with order" |
|||
f" confirmation being a " |
|||
f"prerequisite for providing " |
|||
f"the details. The function " |
|||
f"will be triggered after " |
|||
f"confirming the order. " |
|||
f"Upon asking a confirmation " |
|||
f"question and receiving a " |
|||
f"'yes' response, this function" |
|||
f" must be activated.", |
|||
"parameters": { |
|||
"type": "object", |
|||
"properties": { |
|||
"OrderDetails": { |
|||
"type": "object", |
|||
"properties": { |
|||
"Products": { |
|||
"type": "array", |
|||
"items": { |
|||
"type": "object", |
|||
"properties": { |
|||
"Quantity": { |
|||
"type": "number", |
|||
'description': f"This parameter is used to retrieve the product quantity. {{ quantities: customer ordered quantity }}" |
|||
}, |
|||
"Customer": { |
|||
"type": "string", |
|||
'description': f"This parameter is used to retrieve the customer name. {{ customer: customer name }}" |
|||
}, |
|||
"Product": { |
|||
"type": "string", |
|||
'description': f"This parameter is used to retrieve the Product name. {{ product: product name }}", |
|||
}, |
|||
"Variant": { |
|||
"type": "string", |
|||
"description": f"This parameter is used to retrieve the Product variant details. For example, if a customer order is confirmed, then retrieve {{ Variant: product Variant }}", |
|||
}, |
|||
"productId": { |
|||
"type": "number", |
|||
"description": f'Determine the Product ID of the product in the confirmed order. Fetch the correct ID based on the following product and ID mapping: , {self.function_description}', |
|||
} |
|||
}, |
|||
"required": [ |
|||
"Quantity", |
|||
"Customer", |
|||
"Product", |
|||
"Variant", |
|||
"productId"] |
|||
} |
|||
} |
|||
}, |
|||
"required": ["products"] |
|||
} |
|||
}, |
|||
"required": ["OrderDetails"] |
|||
} |
|||
}, |
|||
"server": { |
|||
"url": f"{base_url}/vapi_voice_assistant/details", |
|||
} |
|||
}, |
|||
{ |
|||
"type": "function", |
|||
"async": True, |
|||
"function": { |
|||
"name": "SetUserLanguagePreference", |
|||
"description": "This custom function retrieves the selected languages at the beginning of a voice assistance session if the user chooses the languages at the start of the session. This function must achieve the LanguageCode", |
|||
"parameters": { |
|||
"type": "object", |
|||
"properties": { |
|||
"LanguagePreference": { |
|||
"type": "object", |
|||
"properties": { |
|||
"LanguageCode": { |
|||
"type": "string", |
|||
"description": f"The code of the language chosen by the user. For example: en-IN for English (India), en-US for English (United States), es-LA for Spanish (Latin America).,our lanagues and its code is {self.language_function_description}" |
|||
}, |
|||
}, |
|||
"required": [ |
|||
"LanguageCode", |
|||
] |
|||
} |
|||
}, |
|||
"required": ["LanguagePreference"] |
|||
} |
|||
}, |
|||
"server": { |
|||
"url": f"{base_url}/vapi_voice_assistant/language_details" |
|||
} |
|||
}, |
|||
], |
|||
}, |
|||
"clientMessages": ["conversation-update", "function-call", |
|||
"hang", "model-output", "speech-update", |
|||
"status-update", "transcript", "tool-calls", |
|||
"user-interrupted", "voice-input"], |
|||
"serverMessages": ["conversation-update", "end-of-call-report", |
|||
"function-call", "hang", "speech-update", |
|||
"status-update", "tool-calls", |
|||
"transfer-destination-request", |
|||
"user-interrupted"], |
|||
"messagePlan": { |
|||
"idleMessages": ["Feel free to ask whenever you're ready.", |
|||
"I'm still here if you need assistance.", |
|||
"How can I assist you further?", |
|||
"Are you still there?", |
|||
"Looking for something specific? I can " |
|||
"assist with that!", |
|||
"Need help choosing a product? I'm here " |
|||
"to assist.", |
|||
"I'm here if you need any assistance with" |
|||
"your shopping."], |
|||
"idleTimeoutSeconds": 5, |
|||
"idleMessageMaxSpokenCount": 10}, |
|||
"name": self.name, |
|||
"firstMessage": first_message, |
|||
"serverUrl": f"{base_url}/vapi_voice_assistant/status", |
|||
"endCallPhrases": [ |
|||
self.end_call_phrases |
|||
], |
|||
} |
|||
headers = { |
|||
"Authorization": f"Bearer {bearer}", |
|||
"Content-Type": "application/json" |
|||
} |
|||
response = requests.request("POST", url, json=payload, |
|||
headers=headers) |
|||
json_response = response.json() |
|||
self.write({'date': json_response['createdAt'], |
|||
'state': 'done', |
|||
'id_assistant': json_response['id'] |
|||
}) |
|||
|
|||
def write(self, vals): |
|||
"""Update the external Vapi.ai assistant configuration when |
|||
the record is modified.""" |
|||
res = super().write(vals) |
|||
if self.id_assistant: |
|||
base_url = self.env['ir.config_parameter'].sudo().get_param( |
|||
'web.base.url') |
|||
bearer = self.env['ir.config_parameter'].sudo().get_param( |
|||
'ora_ai_base.vapi_private_api_key') |
|||
url = f"https://api.vapi.ai/assistant/{self.id_assistant}" |
|||
if self.is_lang_switch and self.language_ids: |
|||
language = "en" |
|||
voice = "qgj3VahzWaAK300v6H27" |
|||
first_message = self.first_message |
|||
end_message = self.end_call_phrases |
|||
else: |
|||
language = self.language_id.code |
|||
voice = self.language_id.voice |
|||
first_message = self._translate_text(self.first_message, |
|||
language) |
|||
end_message = self._translate_text(self.end_call_phrases, |
|||
language) |
|||
payload = { |
|||
"transcriber": { |
|||
"provider": self.transcriber_provider, |
|||
"model": self.transcriber_model_id.key, |
|||
"language": language, |
|||
"smartFormat": True}, |
|||
"voice": { |
|||
"voiceId": voice, |
|||
"provider": "11labs"}, |
|||
"model": { |
|||
"provider": self.provider, |
|||
"model": self.provider_model_id.key, |
|||
"knowledgeBase": { |
|||
"provider": "canonical", |
|||
"fileIds": self.file_ids.mapped('id_file')}, |
|||
"messages": [{ |
|||
"role": "system", |
|||
"content": self.prompt}], |
|||
"tools": [{ |
|||
"type": "function", |
|||
"async": True, |
|||
"function": { |
|||
"name": "GetFinalOrderDetails", |
|||
"description": f"This function is designed to" |
|||
f" retrieve order details from" |
|||
f" a voice assistant, with " |
|||
f"order confirmation being a " |
|||
f"prerequisite for providing " |
|||
f"the details. The function " |
|||
f"will be triggered after " |
|||
f"confirming the order. Upon" |
|||
f" asking a confirmation " |
|||
f"question and receiving a " |
|||
f"'yes' response, this function" |
|||
f" must be activated.", |
|||
"parameters": {"type": "object", "properties": { |
|||
"OrderDetails": |
|||
{"type": "object", "properties": |
|||
{"Products": { |
|||
"type": "array", |
|||
"items": { |
|||
"type": "object", |
|||
"properties": { |
|||
"Quantity": { |
|||
"type": "number", |
|||
'description': f"This parameter is used to retrieve the product quantity. {{ quantities: customer ordered quantity }}"}, |
|||
"Customer": { |
|||
"type": "string", |
|||
'description': f"This parameter is used to retrieve the customer name. {{ customer: customer name }}"}, |
|||
"Product": { |
|||
"type": "string", |
|||
'description': f"This parameter is used to retrieve the Product name. {{ product: product name }}"}, |
|||
"Variant": { |
|||
"type": "string", |
|||
"description": f"This parameter is used to retrieve the Product variant details. For example, if a customer order is confirmed, then retrieve {{ Variant: product Variant }}"}, |
|||
"productId": { |
|||
"type": "number", |
|||
"description": f'Determine the Product ID of the product in the confirmed order. Fetch the correct ID based on the following product and ID mapping: , {self.function_description}'}}, |
|||
"required": [ |
|||
"Quantity", |
|||
"Customer", |
|||
"Product", |
|||
"Variant", |
|||
"productId"]}}}, |
|||
"required": ["products"]}}, |
|||
"required": ["OrderDetails"]}}, |
|||
"server": { |
|||
"url": f"{base_url}/vapi_voice_assistant/details"}}, |
|||
{"type": "function", |
|||
"async": True, |
|||
"function": { |
|||
"name": "SetUserLanguagePreference", |
|||
"description": "This custom function retrieves" |
|||
"the selected languages at the " |
|||
"beginning of a voice " |
|||
"assistance session if the user" |
|||
" chooses the languages at the " |
|||
"start of the session. This " |
|||
"function must achieve the " |
|||
"LanguageCode", |
|||
"parameters": { |
|||
"type": "object", |
|||
"properties": { |
|||
"LanguagePreference": { |
|||
"type": "object", |
|||
"properties": { |
|||
"LanguageCode": { |
|||
"type": "string", |
|||
"description": f"The code of the language chosen by the user. For example: en-IN for English (India), en-US for English (United States), es-LA for Spanish (Latin America).,our lanagues and its code is {self.language_function_description}"}, }, |
|||
"required": [ |
|||
"LanguageCode", ]}}, |
|||
"required": ["LanguagePreference"]}}, |
|||
"server": { |
|||
"url": f"{base_url}/vapi_voice_assistant/language_details"}}, ], }, |
|||
"clientMessages": ["conversation-update", "function-call", |
|||
"hang", "model-output", "speech-update", |
|||
"status-update", "transcript", "tool-calls", |
|||
"user-interrupted", "voice-input"], |
|||
"serverMessages": ["conversation-update", "end-of-call-report", |
|||
"function-call", "hang", "speech-update", |
|||
"status-update", "tool-calls", |
|||
"transfer-destination-request", |
|||
"user-interrupted"], |
|||
"messagePlan": { |
|||
"idleMessages": ["Feel free to ask whenever you're ready.", |
|||
"I'm still here if you need assistance.", |
|||
"How can I assist you further?", |
|||
"Are you still there?", |
|||
"Looking for something specific? I can " |
|||
"assist with that!", |
|||
"Need help choosing a product? I'm here" |
|||
" to assist.", |
|||
"I'm here if you need any assistance with" |
|||
" your shopping."], |
|||
"idleTimeoutSeconds": 5, |
|||
"idleMessageMaxSpokenCount": 10}, |
|||
"name": self.name, |
|||
"firstMessage": first_message, |
|||
"serverUrl": f"{base_url}/vapi_voice_assistant/status", |
|||
"endCallPhrases": [ |
|||
end_message], } |
|||
headers = { |
|||
"Authorization": f"Bearer {bearer}", |
|||
"Content-Type": "application/json"} |
|||
response = requests.request("PATCH", url, json=payload, |
|||
headers=headers) |
|||
if response.status_code != 200: |
|||
raise ValidationError( |
|||
"Assistant Update Failed: %s" % response.text) |
|||
return res |
|||
|
|||
def unlink(self): |
|||
"""Ensure the corresponding assistant in the Vapi.ai platform is |
|||
also deleted when the Odoo record is removed.""" |
|||
bearer = self.env['ir.config_parameter'].sudo().get_param( |
|||
'ora_ai_base.vapi_private_api_key') |
|||
for rec in self: |
|||
url = f"https://api.vapi.ai/assistant/{rec.id_assistant}" |
|||
headers = { |
|||
"Authorization": f"Bearer {bearer}"} |
|||
requests.request("DELETE", url, headers=headers) |
|||
return super().unlink() |
|||
|
|||
def action_assistant_testing(self): |
|||
"""Triggers the client-side action to initiate voice assistant |
|||
testing.""" |
|||
bearer = self.env['ir.config_parameter'].sudo().get_param( |
|||
'ora_ai_base.vapi_public_api_key') |
|||
return { |
|||
'type': 'ir.actions.client', |
|||
'tag': 'action_voice_assistant', |
|||
'params': { |
|||
'assistant_id': self.id_assistant, |
|||
'api_key': bearer, |
|||
'assistant_name': self.name}} |
|||
|
|||
@api.model |
|||
def reset_assistant(self, assistant_id): |
|||
""" Reset Assistant with old values. |
|||
After Assistant updated through js in the VAPI end.""" |
|||
if assistant_id: |
|||
rec = self.search([('id_assistant', '=', assistant_id)]) |
|||
rec.write({}) |
|||
@ -0,0 +1,87 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
import requests |
|||
import base64 |
|||
from odoo import api, fields, models |
|||
|
|||
|
|||
class OraFile(models.Model): |
|||
"""Used to store custom files.""" |
|||
_name = "ora.file" |
|||
_description = "Ora File" |
|||
|
|||
name = fields.Char(string="Name", required=True, |
|||
help="A descriptive name for the file.") |
|||
file = fields.Binary(string="File", required=True, |
|||
help="The actual binary file to be stored.") |
|||
id_file = fields.Char(string="File id", readonly=True, |
|||
help="The unique identifier of the file,") |
|||
|
|||
@api.model_create_multi |
|||
def create(self, vals): |
|||
"""Create a new OraFile record and upload the file to the |
|||
VAPI platform.""" |
|||
bearer = self.env['ir.config_parameter'].sudo().get_param( |
|||
'ora_ai_base.vapi_private_api_key') |
|||
res = super().create(vals) |
|||
decoded_data = base64.b64decode(res.file) |
|||
url = "https://api.vapi.ai/file" |
|||
files = { |
|||
"file": (res.name, decoded_data, "application/pdf") |
|||
} |
|||
headers = { |
|||
"Authorization": f"Bearer {bearer}" |
|||
} |
|||
response = requests.post(url, files=files, headers=headers) |
|||
json_response = response.json() |
|||
res.write({'id_file': json_response['id']}) |
|||
return res |
|||
|
|||
def unlink(self): |
|||
"""Delete OraFile record and remove the associated file from |
|||
the VAPI platform.""" |
|||
bearer = self.env['ir.config_parameter'].sudo().get_param( |
|||
'ora_ai_base.vapi_private_api_key') |
|||
for rec in self: |
|||
url = f"https://api.vapi.ai/file/{rec.id_file}" |
|||
headers = { |
|||
"Authorization": f"Bearer {bearer}"} |
|||
requests.request("DELETE", url, headers=headers) |
|||
res = super().unlink() |
|||
return res |
|||
|
|||
def write(self, vals): |
|||
"""Update the OraFile record and propagate changes to |
|||
the VAPI platform.""" |
|||
bearer = self.env['ir.config_parameter'].sudo().get_param( |
|||
'ora_ai_base.vapi_private_api_key') |
|||
res = super().write(vals) |
|||
if self.id_file: |
|||
url = f"https://api.vapi.ai/file/{self.id_file}" |
|||
payload = {"name": self.name} |
|||
headers = { |
|||
"Authorization": f"Bearer {bearer}", |
|||
"Content-Type": "application/json" |
|||
} |
|||
requests.request("PATCH", url, json=payload, |
|||
headers=headers) |
|||
return res |
|||
@ -0,0 +1,69 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from googletrans import Translator |
|||
from odoo import api, fields, models |
|||
|
|||
|
|||
class OraLanguage(models.Model): |
|||
"""Managing supported languages for the AI voice assistant.""" |
|||
_name = "ora.language" |
|||
_description = "Ora Language" |
|||
|
|||
name = fields.Char(string='Name', |
|||
help="The display name of the language" |
|||
" (e.g., English, Spanish).") |
|||
code = fields.Char(string='Code', |
|||
help="The ISO language code used for " |
|||
"translation (e.g., 'en', 'es').") |
|||
first_msg = fields.Char(string='First Message', |
|||
help="The default welcome message that" |
|||
" the assistant will say in this language.") |
|||
voice = fields.Char(string='Voice', |
|||
help="The identifier of the voice profile used " |
|||
"for text-to-speech in this language.") |
|||
|
|||
def _translate_text(self, text, target_lang): |
|||
"""Translates the given text into the specified |
|||
language using Googletrans.""" |
|||
translator = Translator() |
|||
translated = translator.translate(text, dest=target_lang) |
|||
return translated.text |
|||
|
|||
@api.model |
|||
def get_language(self, language): |
|||
""" Retrieves assistant language settings and translated content.""" |
|||
if language: |
|||
rec = self.search([('code', '=', language)]) |
|||
assistant = self.env['ora.ai'].search( |
|||
[('is_lang_switch', '=', True)], limit=1) |
|||
prompt = assistant.prompt.replace( |
|||
"say thankyou for ordering and goodbye", |
|||
self._translate_text("say thankyou for " |
|||
"ordering and goodbye", language)) |
|||
return {"first_msg": rec.first_msg, |
|||
"voice": rec.voice, |
|||
"assistant_prompt": prompt, |
|||
"end_msg": self._translate_text( |
|||
f"{assistant.end_call_phrases}", language), |
|||
"status": True} |
|||
else: |
|||
return {"status": False} |
|||
@ -0,0 +1,36 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from odoo import fields, models |
|||
|
|||
|
|||
class ProviderModel(models.Model): |
|||
"""AI model configurations provided by external AI service providers.""" |
|||
_name = "provider.model" |
|||
_description = "Provider Model" |
|||
|
|||
name = fields.Char(string="Model Name", |
|||
help="Display name of the AI model") |
|||
key = fields.Char(string="Model Key", |
|||
help="Technical identifier used when calling" |
|||
" the AI provider's API ") |
|||
provider = fields.Char(string="Provider", |
|||
help="The name of the AI service provider") |
|||
@ -0,0 +1,38 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from odoo import fields, models |
|||
|
|||
|
|||
class TranscriberModel(models.Model): |
|||
"""speech-to-text model configurations used for transcribing voice input |
|||
in the assistant system.""" |
|||
_name = "transcriber.model" |
|||
_description = "Transcriber Model" |
|||
|
|||
name = fields.Char(string="Model Name", help="The display name .") |
|||
key = fields.Char(string="Model Key", |
|||
help="Unique key used to identify the model when" |
|||
" integrating with transcription APIs.") |
|||
provider = fields.Char( |
|||
string="Provider", |
|||
help="The name of the service provider offering this" |
|||
" transcription model (e.g., Deepgram, Gladia).") |
|||
@ -0,0 +1 @@ |
|||
googletrans== 4.0.0rc1 |
|||
|
|
After Width: | Height: | Size: 418 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 495 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 624 B |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 310 B |
|
After Width: | Height: | Size: 929 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 542 B |
|
After Width: | Height: | Size: 576 B |
|
After Width: | Height: | Size: 733 B |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 383 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 911 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 600 B |
|
After Width: | Height: | Size: 673 B |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 926 B |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 878 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 653 B |
|
After Width: | Height: | Size: 800 B |
|
After Width: | Height: | Size: 905 B |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 839 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 427 B |
|
After Width: | Height: | Size: 627 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 988 B |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 875 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 439 B |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 565 B |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 115 KiB |