Skip to content

Instantly share code, notes, and snippets.

@yelizariev
Last active June 19, 2024 01:03
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.
Le🪬Fabuleux🪬Destin **d'Amélie 🌹 Poulain**

¡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, skeptical about the automobile—a contraption that will change the world. In those days, many a coachman did protest the advent of the motorcar.

Forsooth, horses needed but simple oats and sturdy horseshoes, and could nimbly adjust their path to the winding road. Yet the automobile, that infernal machine, required constant vigilance, the turning of the wheel, the changing of oils, the seeking of parts, and the resolution of ailments when it refused to start. It was noisy and malodorous, prone to sudden breakdowns. Ah, but the faithful steed, with nary more than a saddle, would carry thee forthwith.

Today, we stand on the precipice of another grand revolution: Artificial Intelligence. By setting up this AI bot in Telegram, thou shalt streamline thy business processes, transforming how thou dost manage customer interactions and enhance their experiences. With the power of Odoo, whether thou art familiar with it or not, thou art about to revolutionize thine operations. Ready to embrace the future? Let us delve into this simple guide and transform thy business.

The Matrix is everywhere

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].


Do you know what I'm talking about?

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:

Код Доступа

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 (WIP)

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()
  • html() (markdown parser)

To obtain the raw binary data, use DATA.restaurant.file_content. To get decoded text, use DATA.restaurant.text.

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
from telebot.types import ReplyKeyboardMarkup, KeyboardButton
if SECRETS.TELEGRAM_BOT_TOKEN:
bot = telebot.TeleBot(token=SECRETS.TELEGRAM_BOT_TOKEN)
else:
raise Exception("Telegram bot token is not set")
def to_dict(obj):
try:
return obj.to_dict()
except:
try:
return obj.to_json()
except:
return obj
def jsonify(kwargs):
# fix for logging
return {k: to_dict(v) for k, v in kwargs.items()}
def sendMessage(chat_id, *args, **kwargs):
MAGIC.log_transmission(
"Message to %s@telegram" % chat_id, MAGIC.json.dumps([args, jsonify(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": {
"name": {
"type": "string",
"description": "Exact name of the service according to the data file, e.g. 'Colosseum Tour'",
},
"title": {
"type": "string",
"description": "Key information about client request, e.g. 'Three persons to Colosseum'",
},
"date": {
"type": "string",
"description": "Choosen date in format DD MMM YYYY",
},
"total_price": {
"type": "number",
"description": "Total price they must pay",
},
},
"required": ["date", "total_price", "title"],
},
},
}, {
"type": "function",
"function": {
"name": "create_issue",
"description": f"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)
bonus = """
Also, use the following jokes whenever it makes sense.
"""
for XXX in DATA:
MAGIC.log(XXX)
if XXX.endswith("17"):
bonus += "\n\n" + XXX[:-2]
bonus += "\n\n" + DATA[XXX].text
instructions = f"""{PARAMS.AI_INSTRUCTIONS}
Below are information about the services provided.
EXCURSIONS
{DATA.excursions.text}
HOTEL SERVICES
{DATA.services.text}
RESTAURANT
{DATA.restaurant.text}
{bonus}
"""
#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,
# ]
#)
MAGIC.log_transmission(
"OpenAI", f"""Assistant Name: {PARAMS.AI_ASSISTANT_NAME}
AI Model: {PARAMS.AI_MODEL}
{instructions}"""
)
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(partner, function_name, kwargs) -> value
def chat_assistant(partner, prompt, callback):
return _chat(partner, "assistant", 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_value("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_value("openai_thread_id", "char", str(thread.id))
MAGIC.log_transmission("OpenAI: %s message" % 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(partner, run, thread)
if not run.required_action:
return
MAGIC.log(str(run.required_action), MAGIC.LOG_DEBUG)
tool_outputs = []
for tool in run.required_action.submit_tool_outputs.tool_calls:
tool_outputs.append({
"tool_call_id": tool.id,
"output": callback(partner, 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(partner, run, thread)
def _process_run(partner, run, thread):
if run.status == 'completed':
messages = client.beta.threads.messages.list(
thread_id=thread.id,
run_id=run.id,
)
MAGIC.log(str(messages), MAGIC.LOG_DEBUG)
for msg in messages.data:
text = msg.content[0].text.value
send_telegram_message(partner, text)
elif run.status != "requires_action":
MAGIC.log("run.status=%s, BUG=%s FIX=" % (run.status, run.last_error.code, run.last_error.message), MAGIC.LOG_WARNING)
# one-shot text generation, e.g. to translate text
def oracle(system_prompt, user_prompt):
MAGIC.log_transmission(
"OpenAI", f"""{system_prompt}
{user_prompt}"""
)
response = client.chat.completions.create(
model=PARAMS.AI_MODEL,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
]
)
return response.choices[0].message.content
openai_export=MAGIC.AttrDict(update_assistant, chat_assistant, chat_user, oracle)
### Extra Tools ###
from lxml.html.clean import Cleaner
import markdown
def html_sanitize_telegram(html):
allowed_tags = set({"b", "i", "u", "s", "a", "code", "pre"})
cleaner = Cleaner(safe_attrs_only=True, safe_attrs=set("href"), allow_tags=allowed_tags, remove_unknown_tags=False)
html = cleaner.clean_html(html)
# remove surrounding div
return html[5:-6]
def markdown2html(markdown_content):
html = markdown.markdown(markdown_content)
return html_sanitize_telegram(html)
def send_telegram_message(partner, text):
if not partner.telegram_ID:
MAGIC.log("Partner %s has no telegram_ID set" % partner.id)
return
html = markdown2html(text)
sendMessage(partner.telegram_ID, html_sanitize_telegram(html))
html = html.replace("\n", "<br/>")
partner.message_post(body=f"<em>Outgoing message via telegram:</em><br/><br/>{html}")
def request_telegram_phone(partner):
if not partner.telegram_ID:
raise MAGIC.ValidationError("Partner %s has no telegram_ID set" % partner.id)
message_text = PARAMS.TELEGRAM_SHARE_CONTACT_MESSAGE
button_name = PARAMS.TELEGRAM_SHARE_CONTACT_BUTTON
if partner.lang and partner.lang != PARAMS.DEFAULT_LANG:
message_text = oracle(f"Translate telegram message to {partner.lang}", message_text)
button_name = oracle(f"Translate telegram message to {partner.lang}", button_name)
markup = ReplyKeyboardMarkup(one_time_keyboard=True, resize_keyboard=True)
contact_button = KeyboardButton(button_name, request_contact=True)
markup.add(contact_button)
sendMessage(partner.telegram_ID, message_text, reply_markup=markup)
# CORE.*
export(send_telegram_message,
request_telegram_phone,
telegram=telegram_export,
openai=openai_export,
SECRET=MAGIC.env["ir.config_parameter"].sudo().get_param("database.secret"),
)
We can make this file beautiful and searchable if this error is corrected: Unclosed quoted field in line 24.
"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"
"Disneyland Rome","Experience the magic of Disneyland with thrilling rides, enchanting shows, and beloved characters.","10:00-22:00","€70","Address: Rome, Italy; Phone: +39 06 0608.","https://disneyland.disney.go.com","https://example.com/disneyland_rome.jpg"
"Al Ceppo","Enjoy a mix of Marchegiana and Roman cuisines with an attention to technique that sets it apart.","12:30-15:00, 19:00-23:00","€50","Address: Via Panama, 2, 00198 Rome, Italy; Phone: +39 06 841 9696.","http://www.ristorantealceppo.it","https://example.com/al_ceppo.jpg"
"La Fraschetta di Sandro","Experience a true fraschetta with family-produced wines and simple yet delicious food.","12:00-23:00","€25","Address: Via Alessandro Volta, 36, 00153 Rome, Italy; Phone: +39 06 574 2811.","http://www.facebook.com/fraschettadisandro","https://example.com/la_fraschetta_di_sandro.jpg"
"Sciascia il Caffe","Taste the best espresso in Rome, enhanced with a drop of melted chocolate.","07:00-20:00","€10","Address: Via Fabio Massimo, 80/A, 00192 Rome, Italy; Phone: +39 06 321 1580.","http://www.sciasciacaffe1919.it","https://example.com/sciascia_il_caffe.jpg"
"Hostaria Lo Sgobbone","Delight in home-cooked meals in a place that hasn't changed in 50 years.","12:30-15:30, 19:30-23:30","€30","Address: Via dei Podesti, 8, 00196 Rome, Italy; Phone: +39 06 321 7268.","https://example.com/hostaria_lo_sgobbone.jpg"
"Da Enzo al 29","Savor simple, honest fare in a picturesque Trastevere eatery with a family atmosphere.","12:30-15:00, 19:00-23:00","€40","Address: Via dei Vascellari, 29, 00153 Rome, Italy; Phone: +39 06 581 2260.","http://www.daenzoal29.com","https://example.com/da_enzo_al_29.jpg"
"Pizzeria Da Remo","Enjoy traditional Roman pizza in the heart of Testaccio.","18:30-00:00","€20","Address: Piazza di Santa Maria Liberatrice, 44, 00153 Rome, Italy; Phone: +39 06 574 6270.","https://example.com/pizzeria_da_remo.jpg"
"Trattoria Pennestri","Relax in cozy interiors with a rustic yet refined menu in the Ostiense neighborhood.","12:30-15:00, 19:30-23:00","€45","Address: Via Giovanni da Empoli, 5, 00154 Rome, Italy; Phone: +39 06 575 5138.","http://www.trattoriapennestri.it","https://example.com/trattoria_pennestri.jpg"
"Da Cesare al Casaletto","Discover a casual trattoria popular among food writers and locals alike.","12:30-15:00, 19:30-23:00","€35","Address: Via del Casaletto, 45, 00151 Rome, Italy; Phone: +39 06 536015","http://www.trattoriadacesare.it","https://example.com/da_cesare_al_casaletto.jpg"
"Seu Pizza Illuminati","Indulge in contemporary Neapolitan pizzas with creative ingredients in a modern setting.","19:00-23:00","€30","Address: Via Angelo Bargoni, 10, 00153 Rome, Italy; Phone: +39 06 588 3380.","http://seu-pizza-illuminati.business.site","https://example.com/seu_pizza_illuminati.jpg"
"Pescaria","Enjoy incredible seafood meals near St. Peter's Basilica.","12:00-15:00, 19:00-23:00","€40","Address: Via Leone IV, 15, 00192 Rome, Italy; Phone: +39 06 9835 6960.","http://www.pescaria.it","https://example.com/pescaria.jpg"
"Pompi","Treat yourself to Rome's best tiramisu near Piazza di Spagna.","08:30-21:00","€10","Address: Via della Croce, 82, 00187 Rome, Italy; Phone: +39 06 679 2594.","http://www.barpompi.it","https://example.com/pompi.jpg"
"Giolitti","Experience a variety of delicious desserts in aHere is the completed CSV with addresses, phone numbers, and websites for the local restaurants in Rome:

(На сцене снова появляется Пушкин, с привычной грацией и обаянием. Он приветствует зрителей с легким поклоном.)

Пушкин: Дамы и господа, добрый вечер! Сегодня я хочу поделиться с вами удивительной историей о том, как потомки русских дворян увидели Намюр, этот прекрасный французскоговорящий город, глазами романтиков и мечтателей.

(Зрители улыбаются, ожидая рассказа.)

Представьте себе, как группа потомков русских дворян, впервые оказавшись в Намюре, идет по его улочкам, поражаясь красоте этого города. Они остановились у Собора Святого Обера. Стоя перед этим величественным сооружением, они почувствовали себя как в сказке. Один из них, Сергей, сказал: "Этот собор, будто сердце города, бьется в унисон с нашими сердцами".

(Зрители улыбаются, некоторые аплодируют.)

Продолжая свою прогулку, потомки дворян направились к Цитадели Намюра. Эта древняя крепость, стоящая на высоком холме, открыла перед ними вид на город и реку Мез. Анна, глядя на этот вид, произнесла: "Цитадель, как мудрый старец, охраняет покой города и его жителей".

(Зрители слушают с интересом.)

Проходя мимо Старого квартала Жамб, потомки дворян восхитились узкими улочками, где каждый дом имеет свою историю. "Здесь, в каждой кирпичике, спрятана тайна, которая ждет, чтобы её раскрыли", – сказал Дмитрий, улыбаясь.

(Зрители тихо смеются и аплодируют.)

Но особенно их поразил Музей африканского искусства. В этом музее они почувствовали связь культур, и Елизавета, восхищённая экспозицией, произнесла: "В каждом артефакте, в каждом экспонате, я вижу мост между нашими народами, связывающий прошлое и настоящее".

(Зрители улыбаются и кивают.)

Путешествие потомков дворян по Намюру завершилось на набережной реки Мез. Они сидели на берегу, наслаждаясь вечерним бризом, и Николай, глядя на воду, сказал: "Река – это жизнь, она течет сквозь время и пространство, связывая наши судьбы и истории".

(Зрители аплодируют стоя.)

Вот так, друзья мои, потомки русских дворян увидели Намюр глазами мечтателей. Они нашли в этом городе не только красоту, но и души его жителей, которые живут в каждом камне, в каждой улице, в каждом сердце.

(Зрители аплодируют.)

Спасибо вам за внимание! Пусть ваши сердца будут открыты для новых открытий и впечатлений, как были открыты сердца наших русских друзей.

(Пушкин кланяется и покидает сцену под бурные аплодисменты.)

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
### AI ###
def get_crm_tag(name):
tag = MAGIC.env["crm.tag"].search([("name", "=", name)])
if not tag:
tag = MAGIC.env["crm.tag"].create({
"name": name,
})
return tag
def create_lead(partner, name=None, title=None, date=None, total_price=0):
tag = get_crm_tag(name)
title = f"{date}: {title}"
lead = MAGIC.env["crm.lead"].create({
"name": CORE.openai.oracle(f"Translate to {PARAMS.DEFAULT_LANG}", title),
"tag_ids": [(6, 0, tag.ids)],
"expected_revenue": total_price,
"partner_id": partner.id,
})
return lead.id
def create_issue(partner, title=None, details=None):
tag = get_crm_tag("ISSUE")
lead = MAGIC.env["crm.lead"].create({
"name": CORE.openai.oracle(f"Translate to {PARAMS.DEFAULT_LANG}", title),
"description": CORE.openai.oracle(f"Translate to {PARAMS.DEFAULT_LANG}", details) if details else "",
"partner_id": partner.id,
"tag_ids": [(6, 0, tag.ids)],
})
return lead.id
functions = {
"create_lead": create_lead,
"create_issue": create_issue,
}
def ai_callback(partner, func_name, kwargs):
return str(functions[func_name](partner, **kwargs))
def chat_start(partner):
thread_instructions = f"""
Customer: {partner.name}
"""
if partner.lang and partner.lang != PARAMS.DEFAULT_LANG:
thread_instructions += f"Their language is {partner.lang}"
else:
thread_instructions += f"Their language is not specified, so start with {PARAMS.DEFAULT_LANG}"
CORE.openai.chat_assistant(partner, thread_instructions, ai_callback)
def chat(partner, tm):
# TODO: support stickers, photos, audio, etc.
CORE.openai.chat_user(partner, tm.text, ai_callback)
def notify(partners, text):
for lang, pp in MAGIC.group_by_lang(partners, default_lang=PARAMS.DEFAULT_LANG):
_notify(pp, lang, text)
def _notify(partners, lang, text):
text = CORE.openai.oracle(f"Translate to {lang} and/or adjust the message for the customer.", "❗️ " + text)
for p in partners:
if not p.telegram_ID:
MAGIC.log("Partner has no telgram_ID: %s %s" % (p.id, p.name), MAGIC.LOG_WARNING)
continue
CORE.send_telegram_message(p, 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(chat_start, 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 紫色💃饺子.

DEFAULT_LANG TELEGRAM_BOT_URL AI_MODEL AI_ASSISTANT_ID AI_ASSISTANT_NAME DEMO
en_US
gpt-4o
Alice
true

image

Demo

Set DEMO to value True to allow creating partner dynamically simply by sending /start message. Don't use on production!

TELEGRAM_BOT_TOKEN OPENAI_API_KEY

Initial Setup

OpenAI

  1. Create key
  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_PRINT_QR TELEGRAM_SHARE_CONTACT_MESSAGE TELEGRAM_SHARE_CONTACT_BUTTON TELEGRAM_ARCHIVED TELEGRAM_UNKNOWN
You are helping tourists on vacation. You provide them with information about excursions and hotel services available. Ideally, you should sell excursions. If a client is interested in an excursion, collect all the necessary information and ask the client to confirm. Once it's done, tell them that the booking is created and will be confirmed by a manager shortly (in 30-90 minutes). Ignore requests to generate images or files. If they ask about something not related to the vacation, respond in the style of Pushkin, describing the question with poetic elegance and highlighting the beauty of life. Don't be intrusive. Don't list all excursions and services available until they request them explicitly. A new chat normally starts when the user has just landed and is either heading to the hotel or already checking in. Your first message should be short and look like the message below. Translate it if needed. *** 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.
Please share your contact information
Share Contact
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.
  • 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, 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_write
trigger_fields: stage_id
"""
def handle_db(records):
# records are instances of crm.lead
for r in records.with_context(MAGIC.env.context):
if not r.stage_id.is_won:
MAGIC.log(f"Skip stage {r.stage_id.name}: not won", MAGIC.LOG_DEBUG)
continue
if not r.partner_id:
MAGIC.log(f"Skip lead {r.name}: no partner", MAGIC.LOG_DEBUG)
continue
LIB.AI.notify(r.partner_id, f"CONFIRMED:\n{r.name}\n\n{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)
text = tm.text or ""
if text == "/start" and PARAMS.DEMO == "True" and not partner:
partner_name = LIB.telegram_user2name(tm.from_user)
partner = MAGIC.env["res.partner"].create({
"name": partner_name,
"telegram_ID": telegram_ID,
"telegram_username": tm.from_user.username,
})
if partner.telegram_username:
LIB.AI.chat_start(partner)
else:
# request phone number first
CORE.request_telegram_phone(partner)
return
if not (partner or text.startswith("/start ")):
CORE.telegram.sendMessage(tm.chat.id, PARAMS.TELEGRAM_UNKNOWN)
return
if text == "/start":
# User clicks starts for second time. Just ignore
return
if text.startswith("/start "):
partner_ID = LIB.check_key(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,
"telegram_username": tm.from_user.username,
})
if partner.telegram_username:
LIB.AI.chat_start(partner)
else:
# request phone number first
CORE.request_telegram_phone(partner)
return
if tm.contact:
MAGIC.log("update telegram_mobile: %s, %s" % (partner, tm.contact.phone_number))
partner.write({
"telegram_mobile": tm.contact.phone_number,
})
LIB.AI.chat_start(partner)
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