In this article, we will describe how to build your first module for FinanceGPT 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
Table of Contents:
Ensure a reachable Webhook endpoint (not needed in local dev setup)
To do that we need to create a connection between Unique FinanceGPT 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 FinanceGPT 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 --- ngrok (Ctrl+C to quit) K8s Gateway API support available now: https://ngrok.com/r/k8sgb Session Status online Account Andreas Hauri (Plan: Free) Update update available (version 3.9.0, Ctrl-U to update) Version 3.8.0 Region Europe (eu) Latency 13ms Web Interface http://127.0.0.1:4040 Forwarding https://cbac-178-197-218-164.ngrok-free.app -> http://localhost:5001 Connections ttl opn rt1 rt5 p50 p90 543 0 0.00 0.00 2.40 45.26 HTTP Requests ------------- POST /webhook 200 OK POST /webhook 200 OK
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
Dont worry my app will be deleted after writing this so you can ignore it.
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! you can look it up in any graphql request on the front end like in the picture below (this is the one from the next multi-tenant):
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: https://unique-ch.atlassian.net/wiki/spaces/PUB/pages/586186776/AI+Module+Templates#Creating-an-AI-Module-Template.
Create a Module Template via API (Deprecated)
Now we need to make sure that you can create the module that will react to the event in a space.
For this, we need a bit of API magic for the moment.
Here is the simplest way. Copy your bearer Token from the browser illustrated in the picture below:
Run the GraphQL via Postman
Now you will need to execute a graphql request like this one for example in Postman:
Make sure you have set the correct URL:
Local dev setup, use
http://localhost:8092/graphql
For remote setup, see the graphql request from the browser like above
The name here is very important. Your app will receive a payload that contains this name should it be called upon.
the descriptions are also very important. Here you see that this function will only be called if asked for a demo. This is up to you to provide and also test how well it reacts to its intent.
And make sure you have set the correct Bearer token like in the example below, which is the token copied from above.
There is no Bearer in front and no carriage return at the end!
Here is the mutation as text:
mutation CreateModuleTemplate { createModuleTemplate( input: { configuration: { languageModel: "AZURE_GPT_35_TURBO_16K" } isExternal: true name: "DemoApp" templateName: "Demo App Template Name" toolDefinition: { type: "function" function: { name: "DemoApp" parameters: { type: "object" properties: { instruction: { type: "string" description: "The instruction given by the employee e.g., 'Demo my App please' or 'Tell me about the demo'." } } } description: "This function is specifically designed for demos" } } weight: 5500 } ) { templateName companyId } }
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. Dont worry, only you can see this space for now.
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 biggest source of mistakes usually!
Look at the prefixes of the key app using
Remember how a correct API_BASE is defined
90% of the errors happen here 😃
API_KEY=ukey_qXqoh5lGhTa399CPNHHLUqnkIaWXlwRwAWtPJSrubyM APP_ID=app_ueltalg142m341pskcankxk0 API_BASE=https://gateway.oleole.unique.app/public/chat ENDPOINT_SECRET=usig_ad4wJ17fETob6uh0CYasNVn_aYtU1MRH4YnMrMiXjNE
Now go to the terminal and execute the following in that folder:
➜ assistant_demo git:(main) ✗ poetry run flask run --port 5001 --debug Creating virtualenv assistant-demo-C-sDUm3y-py3.12 in /Users/andreashauri/Library/Caches/pypoetry/virtualenvs Command not found: flask ➜ assistant_demo git:(main) ✗ poetry install Installing dependencies from lock file Package operations: 17 installs, 0 updates, 0 removals - Installing markupsafe (2.1.5) - Installing blinker (1.7.0) - Installing certifi (2024.2.2) - Installing charset-normalizer (3.3.2) - Installing click (8.1.7) - Installing idna (3.6) - Installing itsdangerous (2.1.2) - Installing jinja2 (3.1.3) - Installing packaging (24.0) - Installing typing-extensions (4.10.0) - Installing urllib3 (2.2.1) - Installing werkzeug (3.0.1) - Installing flask (3.0.2) - Installing gunicorn (21.2.0) - Installing python-dotenv (1.0.1) - Installing requests (2.31.0) - Installing unique-sdk (0.7.0) Installing the current project: assistant_demo (0.1.0) ➜ assistant_demo git:(main) ✗ poetry run flask run --port 5001 --debug * Serving Flask app 'assistant_demo/app.py' * Debug mode: on WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on http://127.0.0.1:5001 Press CTRL+C to quit * Restarting with stat * Debugger is active! * Debugger PIN: 346-338-416
Note that the app is running on port 5001 on localhost but remember ngrock is redirecting the information.
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: # Only verify the event if there is an endpoint secret defined # Otherwise use the basic event deserialized with json 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.user-message.created" and event["payload"]["assistantId"] == assistant_id ): message = event["payload"]["text"] app.logger.info(f"Received message: {message}") # Send a message back to the user unique_sdk.Message.create( user_id=event["userId"], company_id=event["companyId"], chatId=event["payload"]["chatId"], assistantId=assistant_id, text="Hello from the Assistant Demo! 🚀", role="ASSISTANT", ) return "OK", 200
Check now to make sure that the app does:
Not react to
unique.chat.user-message.created
but to aunique.chat.external-module.chosen
event.React only if it is the defined
DemoApp
Ensure that the
assistant_id
is taken from the event and not from the env variablesThe text is now on the user message object
event["payload"]["userMessage"]["text"]
We also echo the message back to the user
Here the last part changes from lines 72 to 89:
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}") # Send a message back to the user 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", )
Save and the flask app should automatically restart.
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 -
The frontend of Unique:
Author |
---|