How to build my first module With SDK and Webhooks — Unique AI Documentation
How to build my first module With SDK and Webhooks
In this article, we will describe how to build your first module for Unique AI so you can run your own code against the chat.
As an example, we are going to deploy the most simple app from this repository the demo app: https://github.com/Unique-AG/sdk-deploy-template
Ensure a reachable Webhook endpoint
Should you be unable to use ngrok in your dev environment please use the SEE connection. You find more information in our GitHub.
To do that we need to create a connection between Unique AI and your development environment. For this we use ngrok but you can also use any other way to get a webhook redirected to your development machine. E.g. Azure functions or other mechanisms. It is important that the data can connect from your Unique AI environment to your local machine somehow.
Here is an example with ngrok: this forwards the https://cbac-178-197-218-164.ngrok-free.app to your local 5001 port, where the apps will run.
ngrok http 5001
Setting up the app
Now login to Unique and go to the app registration. This requires admin rights to do so.
Create a new app
Activate the app
Add an endpoint
Make sure you know the URL that your machine is exposing, here it is the ngrok URL but it is webhook because the demo application exposes that.
In development was chosen so the webhook does not expire on too many errors.
The unique.chat.external-module.chosen is the event we are expecting.
Now you can see that it created a key named using_*** which you can expose. This is used for validating on the demo application side that the request is coming from this unique instance and from nowhere else.
Create an API key
Now you create an API KEY
You will only see this once! Copy the key and store it SECURELY. This has the potential to leak a lot of data to the world.
Remember all the variables for the Python App
Now we need all this info (yes this is the .env variable for your app!):
API_KEY=ukey_qXqoh5lGhTa399CPNHHLUqnkIaWXlwRwAWtPJSrubyM
APP_ID=app_ueltalg142m341pskcankxk0
API_BASE=https://gateway.oleole.unique.app/public/chat
ENDPOINT_SECRET=usig_ad4wJ17fETob6uh0CYasNVn_aYtU1MRH4YnMrMiXjNE
API Base must be set correctly
API_BASE is very important to do it right:
Local dev setup:
- Base url:
http://localhost:8092/public
Remote setup:
- no trailing / be careful
- public/chat (not the other way around)
- make sure you use the correct host!
Defining the module
Create a Module Template in the UI
Use the “AI Module Templates” section in the Unique solution’s frontend to create a custom module template for your SDK module. Documentation on this can be found here: AI Module Templates.
Use the Module in a space
Now if you go into the spaces you can choose this as a module so:
Select the Email Writer module first:
Go ahead and publish it.
Select the created Module Template
See the last entry and select this (Demo App Template Name) and also for example the email writer module.
Publish again.
Running your Python app
Now you are all set to run your app locally so it can react to chat messages:
In the location where you cloned the template repo https://github.com/Unique-AG/sdk-deploy-template
The .env file
Go into the directory of the assistant_demo app and add the .env file with your settings:
- All of these are different for you! Make sure they are correct, as 90% of the errors happen here!
- Look at the prefixes of the key app using
Now go to the terminal and execute the following in that folder:
➜ assistant_demo git:(main) ✗ poetry run flask run --port 5001 --debug
Modify the App to act as a Module
We can now edit the app and make some modifications:
This is the app.py of the example we need to modify it in a few places:
import json
import os
from http import HTTPStatus
from logging.config import dictConfig
import unique_sdk
from dotenv import load_dotenv
from flask import Flask, jsonify, request
load_dotenv()
unique_sdk.api_key = os.environ.get("API_KEY")
unique_sdk.app_id = os.environ.get("APP_ID")
if os.environ.get("API_BASE"):
unique_sdk.api_base = os.environ.get("API_BASE")
assistant_id = os.environ.get("ASSISTANT_ID")
if os.environ.get("ENDPOINT_SECRET"):
endpoint_secret = os.environ.get("ENDPOINT_SECRET")
dictConfig(
{
"version": 1,
"root": {"level": "DEBUG", "handlers": ["console"]},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "DEBUG",
}
},
}
)
app = Flask(__name__)
@app.route("/")
def index():
return "Hello from the Assistant Demo! 🚀"
@app.route("/webhook", methods=["POST"])
def webhook():
event = None
payload = request.data
app.logger.info("Received webhook request.")
try:
event = json.loads(payload)
except json.decoder.JSONDecodeError:
return "Invalid payload", 400
if endpoint_secret:
sig_header = request.headers.get("X-Unique-Signature")
timestamp = request.headers.get("X-Unique-Created-At")
if not sig_header or not timestamp:
print("⚠️ Webhook signature or timestamp headers missing.")
return jsonify(success=False), HTTPStatus.BAD_REQUEST
try:
event = unique_sdk.Webhook.construct_event(
payload, sig_header, timestamp, endpoint_secret
)
except unique_sdk.SignatureVerificationError as e:
print("⚠️ Webhook signature verification failed. " + str(e))
return jsonify(success=False), HTTPStatus.BAD_REQUEST
if (
event
and event["event"] == "unique.chat.external-module.chosen"
and event["payload"]["name"] == "DemoApp"
):
message = event["payload"]["userMessage"]["text"]
app.logger.info(f"Received message: {message}")
unique_sdk.Message.create(
user_id=event["userId"],
company_id=event["companyId"],
chatId=event["payload"]["chatId"],
assistantId=event["payload"]["assistantId"],
text=f"Hello from the Assistant Demo! 🚀 echo {message}",
role="ASSISTANT",
)
return "OK", 200
In case you ever modify the .env this does not auto restart the app so you need to do it manually if you change it!
Now chat within the created Demo space
What you see in the console of flask:
* Detected change in '/Users/andreashauri/unique/dev/sdk-deploy-template/assistant_demo/assistant_demo/app.py', reloading
* Restarting with stat
* Debugger is active!
* Debugger PIN: 346-338-416
Received webhook request.
⚠️ Webhook signature verification failed. No signatures found matching the expected signature for payload. Are you passing the raw body you received from Unique? https://unique.ch/docs/webhooks/signatures
Received webhook request.
Received message: make me a demo please
127.0.0.1 - - [15/May/2024 23:53:27] "POST /webhook HTTP/1.1" 400 -
127.0.0.1 - - [15/May/2024 23:53:28] "POST /webhook HTTP/1.1" 200 -
Communication pattern in prod deployment.
Once the built module is deployed in an environment of your choice its important that the unique cluster can communicate with the module. This pattern looks like this:
So that means the custom module must be reachable via https from the unique cluster and the custom Module on the other hand must be able to connect to the unique cluster via https.
Deploying an assistant that uses the SDK
This diagram depicts the general principle of how the SDK can be deployed and operated.
Clients must self-host modules with the SDK on any hyperscaler or even on-premise. The generic process that is modeled above includes five key components:
| Component | Function | Examples |
|---|---|---|
| A code repository | Developers develop the module and then iterate it Most of these tools allow enforcing four-eye principles for developing and deploying separately |
GitHub GitLab Bitbucket On premise Git solution |
| Automation | Runs tests and checks, builds the image, ships it to a registry and triggers a deployment | GitHub Actions GitLab CI Runners Bitbucket Pipelines Jenkins |
| Container Registry | Stores immutable versions of the modules | Azure Container Registry DockerHub |
| Container-running infrastructure | Since its only a docker image, it can be ran literally nearly everywhere, the only requirement being able to run container images. | Azure Container Apps Azure Functions AWS Lambda Google Kubernetes Engine etc. |
| Unique | Calls the modules | Installing and Upgrading Unique |
Unique examples
Clients are free to reuse this code or use it as inspiration to rebuild the same on another Git platform or respective CI/CDs.