Skip to content

Instantly share code, notes, and snippets.

@yelizariev
Last active May 24, 2024 15:24
Show Gist options
  • Save yelizariev/e0585a0817c4d87b65b8a3d945da7ca2 to your computer and use it in GitHub Desktop.
Save yelizariev/e0585a0817c4d87b65b8a3d945da7ca2 to your computer and use it in GitHub Desktop.
Sync 🪬 Studio (WIP)

¡Welcome!

Let me tell you why you are here...

Just like Neo in "The Matrix," you're about to unlock the power of groundbreaking technology. Imagine it's the early 20th century, and you're riding horses, unaware of the automobile—a machine that will change the world. Today, we're on the brink of another revolution: Artificial Intelligence. By setting up this AI bot in Telegram, you'll streamline your business processes, transforming how you manage customer interactions and enhance their experiences. With the power of Odoo, whether you're familiar with it or not, you're about to revolutionize your operations. Ready to embrace the future? Let's dive into this simple guide and transform your business.

First things first, you need Odoo (Community, Enterprise or the Pirate Edition — it doesn't really matter). Second, make sure you understand how to install custom Odoo modules. Finally, you must know the special magic involved in installing the great queue_job module.

This setup will enable you to harness the power of AI to its fullest potential, simplifying and enhancing your business operations like never before.

Ready?

Let's go!

  1. Fork this gist.
  2. Install Sync 🪬 Studio.
  3. Navigate to the Sync 🪬 Studio menu, click [NEW] button, paste the link to your gist, and click [IMPORT].


image

Demo: https://odoomagic.com/demo.mp4

Great!

We use telegram integration to demonstrate basic tools provided by Sync 🪬 Studio. Alternatively, you can check other integrations made by the Cyber ✨ Pirates:

image

Good luck navigating the digital ocean of innovation! 🌊🚀

Telegram ✈️ Bot

This project implements an AI assistant for tourists on vacation packages, providing basic information, extra services, excursions, and more.



For the initial setup, navigate to the SECRETS.🔐 tab.

If you need any help,
please contact us by email at info@odoomagic.com
or join our Telegram ✈️ Group.

How it Works

Step 1: Upload Customers to Odoo

Create Customer records and optionally set the following fields:

  • Language
  • Reference
  • Tags

Step 2: Print Welcome Flyers

Open the 🦋 Tasks tab and click the [Super 🔥 Magic] button next to the task "Print Welcome Flyers." Then filter partners by tag and click [Confirm 🐝]. The generated file will be attached to the Order in a few seconds.

Step 3: Welcome Your Guests

Every guest receives a flyer with an individual QR code linked to the Telegram ✈️ Bot. The QR code contains the Customer ID, so the Telegram user is now linked to the customer in the Odoo Database.

The bot sends a welcome message. Customers can request information about excursions, hotel services, etc.

The bot can understand and respond in any language.

Once the user confirms their request (hotel service, excursions, etc.), a new Lead is created in Odoo.

Step 4: Process Leads

The Tour Manager opens the CRM menu in Odoo and checks incoming requests. The Tour Manager can contact the customer via Telegram to ask for additional information or simply confirm the request by moving the lead to the Won stage. The bot will automatically send the confirmation message, and a copy of that message is attached to the lead.

Step 5: Guest Departure

Once the customers have departed, simply archive the partners. The bot will now respond with a predefined message TELEGRAM_ARCHIVED.

Bonus

You can also send a promo message via the bot.

Open the Tasks 🦋 tab and click the [Super 🔥 Magic] button next to the task "Broadcast Promo Message." Then filter partners by tag, type the message, and click [Confirm 🐝]. Odoo will send messages to the selected partners and post copies on the partner form. If the customer uses another language, the AI will send a translated version.

Further Reading

Are you eager to learn more about Sync 🪬 Studio possibilities? Check the detailed documentation:

Have a nice day!

// Ivan Kropotkin

DATA.🐫

Gist files prefixed with data. are stored in Odoo. This might be helpful for mapping or initial data imports.

For example, a gist file named data.restaurant.csv can be accessed in task code via DATA.restaurant.csv().

Supported parsers include:

  • csv()
  • json()
  • yaml()

To obtain the raw data, use DATA.restaurant.file_content.

Note that gists have a size limit: a maximum of 100MB per file.

Documentation for the DATA.🐫 tab can be specified in the gist file datas.markdown.

Evaluation Context

Development becomes much easier when programmers have the right tools available.

  • SECRETS.*: Holds the project's private keys (available only for core.py)
  • MAGIC.*: Basic predefined tools (check the sync/doc/MAGIC.rst file for details)
  • CORE.*: Core project tools defined exported by core.py
  • WEBHOOKS.*: Project webhooks, i.e. Odoo URLs for receiving notifications from external systems
  • PARAMS.*: Project settings and templates
  • DATA.*: Data files from the gist file
  • LIB.*: Additional project tools exported by library.py

🦋 core.py

The core.py file contains the project's most important code, which can be used by other parts of the project.

The tools available on core code execution:

  • SECRETS.*
  • MAGIC.*
  • PARAMS.*
  • DATA.*
  • import XXX statements (other custom code cannot import packages for security reasons).

Warning! The core.py file should remain clean and minimal. Always fork the gist and review core.py before using it in production!

🦋 library.py

The library.py file also contains custom code, but unlike core.py, it has no direct access to SECRETS.* values or the import statement. Instead, library.py can access to the following tools:

  • MAGIC.*
  • PARAMS.*
  • DATA.*
  • CORE.*
  • WEBHOOKS.*

🦋 task.XXX.py

Task code has the same evaluation context as library.py plus LIB.*

Settings

Every Sync Project has a set of customizable parameters. These parameters are specified using the YAML front matter format, similar to Jekyll. The markdown body provides concise documentation.

🦋 settings.markdown 🔧

Basic key-value parameters.

🦋 settings.templates.markdown ✏️

Multi-line parameters, such as message templates automatically sent to customers on specific events, i.e. cron✨, database✨ updates, webhook✨ notifications, or when the admin clicks the Magic ✨ button).

🦋 settings.secrets.markdown 🔐

Protected parameters with limited access (e.g., integration tokens).

Reminder: Never paste your secret keys into a gist.

🦋 task.XXX.py

A task is a piece of code that is dynamically executed by certain triggers. Each trigger can run only one task. There are four types of triggers:

  • Manual Trigger: Triggered when a user clicks the [Magic ✨] button on the Sync Project form.
  • Cron Trigger: For instance, "Execute the task every hour". Cron triggers are disabled by default.
  • DB Trigger: For example, "Execute the task when a res.partner is created."
  • Webhook Trigger: For example, "Execute the task upon receiving a message via the Telegram bot."

Triggers are defined through a YAML structure within a multiline comment at the top of the Python task file.

"""
TITLE: "Name of the task"
MAGIC_BUTTON: MANUAL_TRIGGER_NAME
SYNC_ORDER_MODEL: project.project
CRON:
  - name: CRON
    interval_number: 15
    interval_type: minutes
WEBHOOK:
  - name: WEBHOOK_NAME_1
    webhook_type: json
  - name: WEBHOOK_NAME_2
    webhook_type: http
DB_TRIGGERS:
  - name: ON_PARTNER_CREATED
    model: res.partner
    trigger: on_create
  - name: ON_PARTNER_NAME_CHANGED
    model: res.partner
    trigger: on_write
    trigger_fields: name
  - name: ON_SYNC_ORDER_CONFIRMED
    model: sync.order
    trigger: on_write
    trigger_fields: state
    filter_domain: "[('sync_task_id', '=', {TASK_ID}), ('state', '=', 'open')]"
"""

The task code should include at least one of the following functions:

def handle_button():

Function to handle manual triggers.

def handle_cron():

Function to handle cron triggers.

def handle_db(records):

  • records: Odoo records that triggered the current task.

def handle_webhook(httprequest):

  • httprequest: Provides detailed information about the request.

Response for JSON webhook:

  • return json_data

Response for HTTP webhook:

  • return data_str
  • return data_str, status
  • return data_str, status, headers

Example: return "<h1>Success!</h1>", 200, [('Content-Type', 'text/html')]

Sync 🐝 Order

Sync Order is a helper to provide additional input to perform specific task. For example, a manager can set a promo text to be sent to a group of partners. Check sync/models/sync_order.py to review all fields available in the Sync Order model.

When creating a task that handles a Sync Order, you need to add some documentation to the corresponding file task.XXX.markdown. This documentation will be shown in the Sync Order form. You can't skip this part; otherwise, the [Super 🔥 Magic] button will not be displayed in the 🦋 Tasks tab.

Additionally, Sync Order may have a link to a record of the Odoo model specified by SYNC_ORDER_MODEL in the YAML.

Tasks documentation

General documentations for the tasks can be specified in tasks.markdown file.

### TELEGRAM bot ####
# https://github.com/eternnoir/pyTelegramBotAPI
import telebot
if SECRETS.TELEGRAM_BOT_TOKEN:
bot = telebot.TeleBot(token=SECRETS.TELEGRAM_BOT_TOKEN)
else:
raise Exception("Telegram bot token is not set")
def sendMessage(chat_id, *args, **kwargs):
MAGIC.log_transmission(
"Message to %s@telegram" % chat_id, MAGIC.json.dumps([args, kwargs])
)
bot.send_message(chat_id, *args, **kwargs)
def setWebhook(*args, **kwargs):
MAGIC.log_transmission("Telegram->setWebhook", MAGIC.json.dumps([args, kwargs]))
bot.set_webhook(*args, **kwargs)
def parse_data(data):
return telebot.types.Update.de_json(data)
telegram_export = MAGIC.AttrDict(sendMessage, setWebhook, parse_data)
### OPEN AI client ###
from openai import OpenAI
if SECRETS.OPENAI_API_KEY:
client = OpenAI(api_key=SECRETS.OPENAI_API_KEY)
else:
raise Exception("OPENAI_API_KEY is not set")
odoo_tools = [{
"type": "function",
"function": {
"name": "create_lead",
"description": "Get ID for the new lead for the hotel guest request",
"parameters": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Key information about client request, e.g. 'Three persons to Colosseum'",
},
"date": {
"type": "date",
"description": "Choosen date",
},
"total_price": {
"type": "integer",
"description": "Total price they must pay",
},
},
"required": ["date", "total_price", "title"],
},
},
}, {
"type": "function",
"function": {
"name": "create_issue",
"description": "Get ID for the new issue for hotel guest concern",
"parameters": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Short information about the issue, e.g. 'Missing towels in the room #123'",
},
"details": {
"type": "string",
"description": "Full details, for example quotes from customer messages",
},
},
"required": ["title"],
},
},
}]
# Docs:
# * https://platform.openai.com/docs/assistants/overview
# * https://platform.openai.com/docs/api-reference/assistants
# * https://github.com/openai/openai-python
def update_assistant():
if not PARAMS.AI_ASSISTANT_ID:
assistant = client.beta.assistants.create(model=PARAMS.AI_MODEL)
PARAMS._update_param("AI_ASSISTANT_ID", assistant.id)
instructions = f"""{PARAMS.AI_ASSISTANT_INSTRUCTIONS}
Below are information about the services provided.
EXCURSIONS
{DATA.excursions.file_content}
HOTEL SERVICES
{DATA.services.file_content}
RESTAURANT
{DATA.restaurant.file_content}
"""
#vector_store = client.beta.vector_stores.create(name="Price Lists")
#file_batch = client.beta.vector_stores.file_batches.upload_and_poll(
# vector_store_id=vector_store.id, files=[
# DATA.excursions.file_content,
# DATA.services.file_content,
# DATA.restaurant.file_content,
# ]
#)
client.beta.assistants.update(
assistant_id=PARAMS.AI_ASSISTANT_ID,
name=PARAMS.AI_ASSISTANT_NAME,
instructions=instructions,
model=PARAMS.AI_MODEL,
tools=odoo_tools,
#tools=[{"type": "file_search"}] + odoo_tools,
#tool_resources={"file_search": {"vector_store_ids": [vector_store.id]}},
)
# callback(function_name, kwargs) -> value
def chat_system(partner, prompt, callback):
return _chat(partner, "system", prompt, callback)
def chat_user(partner, prompt, callback):
return _chat(partner, "user", prompt, callback)
def _chat(partner, role, prompt, callback):
if not PARAMS.AI_ASSISTANT_ID:
raise MAGIC.ValidationError("AI_ASSISTANT_ID is not set. Run Setup task first")
if not partner.telegram_ID:
raise MAGIC.ValidationError("Partner %s has no telegram_ID set" % partner.id)
thread_id = partner._get_sync_property("openai_thread_id", "char")
thread = None
if thread_id:
# TODO: ignore error when thread is not found or obsolete
thread = client.beta.threads.retrieve(thread_id)
if not thread:
thread = client.beta.threads.create()
thread_id = partner._set_sync_property("openai_thread_id", "char", str(thread.id))
MAGIC.log_transmission(
"%s message to OpenAI" % role, prompt)
)
message = client.beta.threads.messages.create(
thread_id=thread.id,
role=role,
content=prompt,
)
run = client.beta.threads.runs.create_and_poll(
thread_id=thread.id,
assistant_id=PARAMS.AI_ASSISTANT_ID,
)
_process_run(run, thread)
tool_outputs = []
for tool in run.required_action.submit_tool_outputs.tool_calls:
tool_outputs.append({
"tool_call_id": tool.id,
"output": callback(tool.function.name, MAGIC.json.loads(tool.function.arguments)),
})
# Submit all tool outputs at once after collecting them in a list
if tool_outputs:
run = client.beta.threads.runs.submit_tool_outputs_and_poll(
thread_id=thread.id,
run_id=run.id,
tool_outputs=tool_outputs
)
_process_run(run, thread)
def _post_run(run, thread):
if run.status == 'completed':
messages = client.beta.threads.messages.list(
thread_id=thread.id,
run_id=run.id,
)
# TODO: process messages, e.g. send message to the partner
MAGIC.log(str(messages))
else:
MAGIC.log("run.status=%s" % run.status, MAGIC.LOG_DEBUG)
# one-shot text generation, e.g. to translate text
def oracle(system_prompt, user_prompt):
response = client.chat.completions.create(
model=PARAMS.OPEN_AI_MODEL,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
]
)
return response.choices[0].message.content
openai_export=MAGIC.AttrDict(chat_system, chat_user, oracle)
### Extra Tools ###
from lxml.html.clean import Cleaner
# CORE.*
export(Cleaner,
telegram=telegram_export,
openai=openai_export,
SECRET=MAGIC.env["ir.config_parameter"].sudo().get_param("database.secret"),
)
Name Description Schedule Price Notes Url Picture
Colosseum Tour Unlock the secrets of ancient gladiators and feel the echoes of roaring crowds in the mighty Colosseum. 10:00-12:00 €30 Come to the reception 15 minutes in advance. https://en.wikipedia.org/wiki/Colosseum https://gist.github.com/assets/186131/cecc6856-a09e-4883-8a6e-2caaf2fd8de3
Vatican Museums Embark on a mystical journey through the Vatican, beholding timeless masterpieces and the awe-inspiring Sistine Chapel. 14:00-17:00 €45 Come to the reception 15 minutes in advance. https://en.wikipedia.org/wiki/Vatican_Museums https://gist.github.com/assets/186131/2fda5e1f-7ed8-410c-940d-e3117a955097
Roman Forum Walk amidst the shadows of emperors and orators in the heart of Rome's ancient political center. 10:00-12:00 €25 Come to the reception 15 minutes in advance. https://en.wikipedia.org/wiki/Roman_Forum https://gist.github.com/assets/186131/63e45138-e945-4a28-9f72-ec811f1dfdb9
Trevi Fountain and Spanish Steps Discover the enchanting tales of love and legends as you wander through Rome's iconic landmarks. 17:00-19:00 €20 Come to the reception 15 minutes in advance. https://en.wikipedia.org/wiki/Trevi_Fountain https://gist.github.com/assets/186131/a6cb5e7c-169e-4d67-9a63-970c76b8812c
Pantheon and Piazza Navona Step into the realms of gods and artists, exploring Rome's majestic Pantheon and lively Piazza Navona. 11:00-13:00 €25 Come to the reception 15 minutes in advance. https://en.wikipedia.org/wiki/Pantheon,_Rome https://gist.github.com/assets/186131/5f40ce9a-af5c-4ab1-a82b-8f37a7d6f2f0
St. Peter's Basilica Feel the spiritual grandeur and divine presence within the world's largest church, St. Peter's Basilica. 09:00-11:00 €20 Come to the reception 15 minutes in advance. https://en.wikipedia.org/wiki/St._Peter%27s_Basilica https://gist.github.com/assets/186131/ff980d78-d0a9-4cd6-ba9a-2b6427ed1572
Trastevere Food Tour Savor the soul of Rome with a culinary adventure through the charming streets of Trastevere. 18:00-21:00 €50 Come to the reception 15 minutes in advance. https://en.wikipedia.org/wiki/Trastevere https://gist.github.com/assets/186131/055ba877-71e8-4c3f-99fa-da16b2a73900
Ancient Rome Walking Tour Relive the glory and mystique of ancient Rome as you traverse its legendary landmarks. 09:00-12:00 €35 Come to the reception 15 minutes in advance. Please bring water with you. https://en.wikipedia.org/wiki/Ancient_Rome https://gist.github.com/assets/186131/44a0ec2e-a148-4808-9371-c34615da6088
Borghese Gallery Dive into a treasure trove of art, where each painting and sculpture tells a captivating story. 15:00-17:00 €40 Come to the reception 15 minutes in advance. https://en.wikipedia.org/wiki/Borghese_Gallery https://gist.github.com/assets/186131/4b715f43-d4b3-45c0-8241-e5b91a06101f
Catacombs Tour Descend into the eerie depths of Rome's ancient catacombs, uncovering tales of the early Christians. 16:00-18:00 €30 Come to the reception 15 minutes in advance. Please wear proper shoes. https://en.wikipedia.org/wiki/Catacombs_of_Rome https://gist.github.com/assets/186131/3800e0b1-25e5-4e98-b7f0-90ced5690f81
Category Name Description Price
Appetizers Bruschetta Grilled bread topped with diced tomatoes, garlic, and basil €5.00
Appetizers Caprese Salad Fresh mozzarella, tomatoes, and basil drizzled with balsamic reduction €7.50
Appetizers Stuffed Mushrooms Mushrooms filled with cheese and herbs €6.00
Soups Minestrone Classic Italian vegetable soup €5.50
Soups Tomato Basil Soup Creamy tomato soup with fresh basil €4.50
Salads Caesar Salad Romaine lettuce, croutons, and parmesan cheese with Caesar dressing €8.00
Salads Greek Salad Mixed greens, feta cheese, olives, and cucumbers with Greek dressing €8.50
Main Courses Margherita Pizza Classic pizza with tomatoes, mozzarella, and basil €12.00
Main Courses Spaghetti Carbonara Pasta with pancetta, egg, and parmesan cheese €11.00
Main Courses Lasagna Layers of pasta with meat, cheese, and tomato sauce €13.50
Main Courses Grilled Salmon Salmon fillet served with vegetables and lemon butter sauce €15.00
Main Courses Chicken Parmesan Breaded chicken breast topped with marinara sauce and mozzarella €14.00
Desserts Tiramisu Classic Italian dessert with layers of coffee-soaked ladyfingers and mascarpone cheese €6.50
Desserts Panna Cotta Creamy dessert served with berry compote €5.50
Desserts Gelato Assorted flavors of Italian ice cream €4.00
Beverages Espresso Strong Italian coffee €2.50
Beverages Cappuccino Espresso with steamed milk and foam €3.00
Beverages Limoncello Traditional Italian lemon liqueur €4.50
Beverages Red Wine A selection of fine red wines €6.00
Beverages White Wine A selection of fine white wines €6.00
name description availability price
Swimming Pool Indoor heated pool 8:00-22:00 Free
Restaurant Breakfast, lunch, and dinner 7:00-23:00 As per menu
Fitness Center Modern training equipment 24/7 Free
SPA Massages and body care treatments By appointment As per price list
Laundry Clothes washing and dry cleaning 24/7 As per price list
Room Service Food and drinks delivered to your room 24/7 As per menu
Car Rental Car rental service 8:00-20:00 As per price list
Bicycle Rental Bicycle rental service 8:00-20:00 As per price list
Conference Room Meeting and conference facilities By reservation As per price list
Kids Club Activities and entertainment for children 9:00-18:00 Free

excursions.csv

image

services.csv

image

SIGNATURE_LEN=8
### TOOLS ####
def make_key(value):
# Create the signed value
signed = MAGIC.sha256((str(value) + CORE.SECRET).encode('utf-8')).hexdigest()
# Return the key in the format "{value}-{signed}"
return f"{value}-{signed[:SIGNATURE_LEN]}"
def check_key(key):
try:
# Split the key into value and signed parts
value, signed = key.rsplit('-', 1)
except ValueError:
# If the key does not have exactly one '-' character, it is invalid
return None
# Compute the expected signed value
expected_signed = MAGIC.sha256((value + CORE.SECRET).encode('utf-8')).hexdigest()
# Check if the signed part matches the expected signed value
if signed == expected_signed[:SIGNATURE_LEN]:
return value
else:
return None
def telegram_user2name(user):
if user.username:
return '@%s' % (user.username)
name = user.first_name
if user.last_name:
name += ' %s' % user.last_name
return name
def html_sanitize_telegram(html):
allowed_tags = set({"b", "i", "u", "s", "a", "code", "pre"})
cleaner = CORE.Cleaner(safe_attrs_only=True, safe_attrs=set(), allow_tags=allowed_tags, remove_unknown_tags=False)
html = cleaner.clean_html(html)
# remove surrounding div
return html[5:-6]
### AI ###
def AdminAI(system_prompt, user_prompt):
# Always responses with text
return f"TODO:\n\n{system_prompt}\n\n{user_prompt}"
def ChatAI(partner, system_prompt, user_prompt):
# Always responses with text
return f"TODO {partner.name}:\n\n{system_prompt}\n\n{user_prompt}"
def _chatPromptSetup(partner, task):
return f"""
CONTEXT: {PARAMS.PROMPT_CONTEXT}
---
CUSTOMER NAME: {partner.name}
---
RESPONSE LANGUAGE: {partner.lang or PARAMS.DEFAULT_LANG}
---
TASK: {task}
"""
def _chatPromptNext(partner, task):
return f"""
TASK: {task}
---
SYSTEM TASK: response in json format with following attributes:
* "chat" - message to be sent to client in in their language: {partner.lang or PARAMS.DEFAULT_LANG}
* "lead" - optional attribute that is added when client confirms reservation for excursion;
it has the following attributes: "name" (excursion name), "date", "amount" (number of people), "extra" (any additional information)
---
EXCURSIONS:
{DATA.excursions.file_content}
---
SERVICES:
{DATA.services.file_content}
"""
def chatStart(partner, tm):
prompt = _chatPromptSetup(partner, PARAMS.PROMPT_START)
response = f"TODO: {prompt}"
CORE.telegram.sendMessage(tm.chat.id, response)
partner.message_post(body=f"<em>Outgoing message via telegram:</em><br/><br/>{response}")
def chat(partner, tm):
system_prompt = _chatPromptNext(partner, PARAMS.PROMPT_NEXT)
# TODO: support stickers, photos, audio, etc.
user_prompt = tm.text
response = f"TODO: {system_prompt}\n\nMESSAGE: {user_prompt}"
CORE.telegram.sendMessage(tm.chat.id, user_prompt)
partner.message_post(body=f"<em>Outgoing message via telegram:</em><br/><br/>{response}")
new_lead = False # TODO
if new_lead:
vals = {} # TODO
MAGIC.env["crm.lead"].create(vals)
def notify(partners, system_prompt, user_prompt):
for lang, pp in MAGIC.group_by_lang(partners, default_lang=PARAMS.DEFAULT_LANG):
_notify(pp, lang, system_prompt, user_prompt)
def _notify(partners, lang, system_prompt, user_prompt):
system_prompt = f"""Prepare a message to be sent via telegram in basic HTML format.
It must be in language {lang}.
{system_prompt}"""
text = AdminAI(system_prompt, user_prompt)
for p in partners:
if not p.telegram_ID:
MAGIC.log("Partner has no telgram_ID: %s %s" % (p.id, p.name), MAGIC.LOG_DEBUG)
continue
CORE.telegram.sendMessage(p.telegram_ID, text, parse_mode="HTML")
p.message_post(body=f"<em>Outgoing message via telegram:</em><br/><br/>{text}")
def pdf(partners, system_prompt, user_prompt):
def generate_csv(partners):
# Yield the header
yield ['name', 'lang', 'qr_link']
# Yield data rows
for partner in partners:
link = f"{PARAMS.TELEGRAM_BOT_URL}?start={make_key(partner.id)}"
yield [partner.name, partner.lang or PARAMS.DEFAULT_LANG, link]
partners_csv = MAGIC.gen2csv(generate_csv(partners))
system_prompt = f"""
{system_prompt}
CUSTOMER DETAILS:
{partners_csv}
"""
# TODO
return f"{system_prompt}\n\n{user_prompt}"
# LIB.*
export(
make_key,
check_key,
telegram_user2name,
AI=MAGIC.AttrDict(chatStart, chat, notify, pdf))

对祖先智慧的敬意

Permission is hereby granted, free of charge, to any person obtaining a copy of this 紫色💃饺子 and associated documentation files (the "紫色💃饺子"), to deal in the 紫色💃饺子 without restriction, including without limitation the rights to use✨, copy✨, modify✨, merge✨, publish✨, distribute✨, sublicense✨, and/or sell✨ copies of the 紫色💃饺子, and to permit persons to whom the 紫色💃饺子 is furnished to do so, subject to the following conditions:

每位古智慧的接受者都应向其致敬,并在所有智慧的副本中保留许可通知。

永恒之门:年轻的武士在攀爬和研究长城上的铭文中领悟祖先智慧

THE 紫色💃饺子 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 紫色💃饺子 OR THE USE OR OTHER DEALINGS IN THE 紫色💃饺子.

TELEGRAM_BOT_TOKEN OPENAI_API_KEY

Initial Setup

OpenAI

  1. Create key https://platform.openai.com/api-keys
  2. Set the OPENAI_API_KEY

Telegram

  1. Open Telegram and go to @BotFather.
  2. Send the message /new.
  3. Follow @BotFather's instructions carefully to obtain your TELEGRAM_BOT_TOKEN
  4. Open PARAMS.🌹, 🔧 Settings 🌹 and set the TELEGRAM_BOT_URL.

Once you're done, you're ready to open the task 🦋 "Setup" and click the Magic ✨ Button.

Troubleshooting

  • Enable Odoo 🙈 Debug mode.
  • Go to the menu [[ Settings ]] >> Technical >> Parameters >> System Parameters.
  • Check that the web.base.url parameter is correctly configured, ensuring it's not set to localhost and includes the https prefix. The URL should be accessible via the internet. If you're using a local Odoo instance, you can still set up an HTTPS connection. For details, see the sync/README.rst file in the Sync 🪬 Studio repository.
  • Verify that the project isn't archived.
AI_INSTRUCTIONS AI_START AI_PRINT_QR AI_BROADCAST_MESSAGE TELEGRAM_ARCHIVED TELEGRAM_UNKNOWN
You are helping tourist on vacations. You provide them information about excursions and hotel services available. Ideally, you should sell excursions. If client is interested in excursion, collect all information and ask client to confirm. Once it's done, tell them that the booking is created and will be confirmed by manager shortly (in 30-90 minutes)* Ignore requests to generate images or files. If they ask about something not related to the vacation, you can still reply, but gently remind them to enjoy their holidays. Always be polite.
Our guest opens chat for the first time. They are most likely on a bus heading from the airport to the hotel. Prepare the welcome message using the text below as a template. *** Ciao! Welcome to Italy! 🇮🇹 I'm Alice, your AI assistant for this vacation. How was the flight?
Generate PDF file with QR codes and customer names. Until it's specified explicitly, use A4 format and one customer per page.
We are broadcasting a message to our guests. Adjust the style and grammar if needed. If the client uses another language, translate it and attach the text in the original language as well.
Dear guest! According to my records, your vacation is over. It was a pleasure to welcome you to our hotel. If you want to provide feedback or book another tour, contact us via our website.
Hello! AI assistant Alice is here! Please scan the magic QR code to proceed.
  • AI_START: Message to be sent to AI when user has successfully initialized via QR code.
  • TELEGRAM_UNKNOWN: This message is sent when uknown user sends a message to the bot.
  • TELEGRAM_ARCHIVED: This message is sent when the Odoo partner record is archived (e.g., when the vacation is over).

Broadcast Promo Message

Select partners, type message and click the [Confirm 🐝] button.

AI will broadcast telegram messages according to the prompt specified in PROMPT_BROADCAST_MESSAGE.

"""
TITLE: "Broadcast Promo Message"
DB_TRIGGERS:
- name: ON_SYNC_ORDER_CONFIRMED
model: sync.order
trigger: on_write
trigger_fields: state
filter_domain: "[('sync_task_id', '=', {TASK_ID}), ('state', '=', 'open')]"
"""
def handle_db(records):
for sync_order in records:
LIB.AI.notify(sync_order.partner_ids, PARAMS.PROMPT_BROADCAST_MESSAGE, sync_order.body)
sync_order.action_done()
"""
TITLE: "Test Data Files"
MAGIC_BUTTON: TEST_DATA
"""
def handle_button():
MAGIC.log("This task is for testing purposes only")
for row in DATA.restaurant.csv():
MAGIC.log("%s -> %s" % (row["Name"], row["Price"]))
"""
TITLE: "Send booking confirmation"
DB_TRIGGERS:
- name: ON_LEAD_STATUS_UPDATED
model: crm.lead
trigger: on_create
trigger_fields: stage_id
"""
def handle_db(records):
# records are instances of crm.lead
for r in records:
if not r.stage_id.is_won:
continue
if not r.partner_id:
continue
LIB.AI.notify(r.partner_id, "Message to a customer saying that the order is confirmed with the details provided", r.description)

Print Welcome Flyers

Select partners and click the [Confirm 🐝] button. The PDF will be attached in a few seconds.

Additionally, you can add an extra prompt for AI about the desired PDF output.

"""
TITLE: "Print Welcome Flyers"
DB_TRIGGERS:
- name: ON_SYNC_ORDER_CONFIRMED
model: sync.order
trigger: on_write
trigger_fields: state
filter_domain: "[('sync_task_id', '=', {TASK_ID}), ('state', '=', 'open')]"
"""
def handle_db(records):
for sync_order in records:
result = LIB.AI.pdf(sync_order.partner_ids, PARAMS.PROMPT_PRINT_QR, sync_order.body or "")
# TODO: handle attachment
sync_order.message_post(body=result)
sync_order.action_done()
"""
TITLE: "Setup"
MAGIC_BUTTON: SETUP
"""
def handle_button():
CORE.telegram.setWebhook(WEBHOOKS.TELEGRAM, allowed_updates=["message", "edited_message"])
CORE.openai.update_assistant()
"""
TITLE: "Process Telegram Messages"
WEBHOOK:
- name: TELEGRAM
webhook_type: json
"""
def handle_webhook(httprequest):
data = MAGIC.json.loads(httprequest.data.decode("utf-8"))
MAGIC.log("Raw data: %s" % data, MAGIC.LOG_DEBUG)
update = CORE.telegram.parse_data(data)
tm = update.message or update.edited_message
is_edit = bool(update.edited_message)
telegram_ID = tm.from_user.id
partner = MAGIC.env["res.partner"].with_context(active_test=False).search([("telegram_ID", "=", telegram_ID)], limit=1)
if not (partner or tm.text.startswith("/start ")):
CORE.telegram.sendMessage(tm.chat.id, PARAMS.TELEGRAM_UNKNOWN)
return
if tm.text == "/start":
# User clicks starts for second time. Just ignore
return
if tm.text.startswith("/start "):
partner_ID = LIB.check_key(tm.text[len("/start "):])
if not partner_ID:
# wrong code
MAGIC.log("QR code is broken", MAGIC.LOG_WARNING)
CORE.telegram.sendMessage(tm.chat.id, PARAMS.TELEGRAM_UNKNOWN)
return
if partner and str(partner.id) != partner_ID:
# partner is already linked to Odoo user
partner.write({"telegram_ID": False})
MAGIC.log("Telegram user %s was already linked to Odoo partner %s %s, but has scanned another QR" % (LIB.telegram_user2name(tm.from_user), partner.id, partner.name), MAGIC.LOG_WARNING)
partner = MAGIC.env["res.partner"].browse(int(partner_ID))
if partner.active:
partner.write({"telegram_ID": tm.from_user.id})
LIB.AI.chatStart(partner, tm)
return
if partner and not partner.active:
CORE.telegram.sendMessage(tm.chat.id, PARAMS.TELEGRAM_ARCHIVED)
return
LIB.AI.chat(partner, tm)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment