diff --git a/README.md b/README.md index 69adc84..e70baa6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,35 @@ -# swhub-custom-app-samples +# Software Hub Custom Apps Samples + IBM Software Hub offered custom application capablity in 5.3, the purpose of the git repo is to host the sample applications for end user to download and try out. + +The repo contains collection of custom sample apps that can be deployed into dataplane/phsical location, each folder contains one sample app and README instructions for deployment + +### Pre-requisites to deploy sample applications + +Custom Apps is built on top of remote dataplanes feature. refer [this doc](https://www.ibm.com/docs/en/software-hub/5.2.x?topic=installing-setting-up-remote-physical-location) to understand concept of remote dataplane and physical locations + +`default dataplane` is a dataplane that resides locally on a regular software hub cluster, that are designated for deploying custom apps. for setting up default dataplane, the following component are required: + +- Software Hub Scheduling Service + refer [this doc](https://www.ibm.com/docs/en/software-hub/5.2.x?topic=cluster-installing-shared-components) for how to install scheduling service as cluster component +- Software Hub 5.3 premium cartrige and cpd-cli olm-utils premium image + +To enable default dataplane using cpd-cli: + +1. load olm-utils premium image: `export OLM_UTILS_IMAGE=icr.io/cpopen/cpd/olm-utils-v4:5.3.0-` +2. Run `cpd-cli manage login-to-ocp -u kubeadmin -p --server=` +3. Run `cpd-cli manage enable-premuim-features --license_acceptance=true --features=enable_rdp --operator_ns= --instance_ns= --scheduler_ns=` +4. Create two namespaces mgmt and workload(wl). +5. Run `cpd-cli manage enable-default-dataplane --instance_ns=zen --management_ns=mgmt --workload_ns=wl` this command will create a default physical location - `default-pl` and a default dataplane - `default-dp`. + +overview of custom applocations included in this example, for demostating application types supported +|Name|Application Type|Folder|| +|:---|:---|:---|:---| +| code engine app | dockerfile | [code-engine-app](code-engine-app/README.md) | example application used for deploying into ibm cloud code engine | +| hello world | dockerfile | [hello-world](hello-world/README.md) | the "hello world" application invoked as an api endpoint | +| mcp gateway forge | template, dockerfile | [mcp-gateway-forge](mcp-gateway-forge/README.md) | Proxy and MCP Registry that federates MCP and REST services | +| qna rag chatbot | dockerfile | [qna_rag_chatbot](qna_rag_chatbot/README.md) | chatbot that can answer questions using RAG | +| ruby hello world | template | [ruby-hello-world](ruby-hello-world/README.md) | the "hello world" application written in ruby with web UI | +| wxo external agent| dockerfile | [wxo-external-agent](wxo-external-agent/README.md)| provides chat completions api that can be used as wxo external agent | + + there is also limited support for "kube yaml" apps, similar to what regular helm chart wraps up (limited to plain kube core/v1 and apps/v1 resources etc), examples are not included here. \ No newline at end of file diff --git a/code-engine-app/README.md b/code-engine-app/README.md new file mode 100644 index 0000000..24456d0 --- /dev/null +++ b/code-engine-app/README.md @@ -0,0 +1,76 @@ +# Deploy code engine app to software hub dataplane + +[code engine app](code-engine-handson-sample-app) is an example application used for deploying into ibm cloud code engine, the application interacts with ibm cloud object storage service, providing an UI interface for populating bucket created under object storage service. the same application can be deployed into software hub default dataplane as well + +- before deploying this sample app, you would need to create object storage service in ibm cloud, create a test bucket and collect the following values: + ``` + BUCKETNAME : Bucket Name of IBM Object Storage + COS_ENDPOINT : ENDPOINT of IBM Object Storage, i.e. s3..cloud-object-storage.appdomain.cloud + COS_APIKEY : API Key of IBM Object Storage, this is your ibm cloud api key that has access to object storage service + COS_RESOURCE_INSTANCE_ID : resource instance ID of IBM Object Storage, you may get the instance id from object store instance CRN, it is the UUID that starts after the last single colon and ends before the final double colon + TESTVALUE: of anything to write into object store bucket + ``` +- checkout the [pre-requisites](../README.md#pre-requisites-to-deploy-sample-applications) required before deploying this application into default dataplane + +### Steps to deploy code-engine-app + +1. Run `create-dockerfile-application` command inside the cpd-cli binary to deploy the app +2. Run `./cpd-cli manage create-dockerfile-application --help` to know more about other options available. +3. Run the create-dockerfile-application command + ``` + ./cpd-cli manage create-dockerfile-application \ + --instance_ns=zen \ + --app_name=code-engine-app \ + --app_port=3000 \ + --tls_enabled=false \ + --repo_url=https://github.com/IBM/swhub-custom-app-samples.git \ + --repo_token= \ + --repo_app_dir=code-engine-app/code-engine-handson-sample-app \ + --app_envs='[{"name":"BUCKETNAME","value":""},{"name":"TESTVALUE","value":"mytest"},{"name":"COS_ENDPOINT","value":"s3..cloud-object-storage.appdomain.cloud"},{"name":"COS_APIKEY","value":""},{"name":"COS_RESOURCE_INSTANCE_ID","value":""},{"name":"HOME", "value":"/tmp"}]' \ + --cpu=400m \ + --memory=200Mi \ + --cpu_limit=500m \ + --memory_limit=400Mi + ``` +4. If you see this error in step 5 `-build` pod + ``` + Defaulted container "docker-build" out of: docker-build, git-clone (init), manage-dockerfile (init) + time="2025-10-30T23:58:57Z" level=info msg="Not using native diff for overlay, this may cause degraded performance for building images: kernel has CONFIG_OVERLAY_FS_REDIRECT_DIR enabled" + I1030 23:58:57.938443 1 defaults.go:112] Defaulting to storage driver "overlay" with options [mountopt=metacopy=on]. + Caching blobs under "/var/cache/blobs". + + Pulling image node:17.4 ... + Resolving "node" using unqualified-search registries (/var/run/configs/openshift.io/build-system/registries.conf) + Trying to pull registry.redhat.io/node:17.4... + Trying to pull registry.access.redhat.com/node:17.4... + Trying to pull quay.io/node:17.4... + Trying to pull docker.io/library/node:17.4... + time="2025-10-30T23:59:31Z" level=warning msg="Failed, retrying in 1s ... (1/3). Error: initializing source docker://node:17.4: reading manifest 17.4 in docker.io/library/node: toomanyrequests: You have reached your unauthenticated pull rate limit. https://www.docker.com/increase-rate-limit"` + ``` + + Path the `pull-secret` with public docker username and password + ``` + # DOCKER_CONFIG=$(echo -n 'username:password' | base64 -w0) + oc set data secret/pull-secret -n openshift-config --from-literal=.dockerconfigjson="{\"auths\":{\"docker.io\":{\"auth\":\"$DOCKER_CONFIG\"}}}" + secret/pull-secret data updated + ``` + +5. check pods in workload namespace + + ``` + # oc get po -n wl + NAME READY STATUS RESTARTS AGE + final-code-engine-app-j5itik3ikksx-1-build 0/1 Completed 0 6m32s + final-code-engine-app-j5itik3ikksx-5f79744d79-75gpk 1/1 Running 0 3m49s + ``` + +6. update application proxy config - copy `code-engine-app.conf.yaml` in this folder to `cpd-cli-workspace/olm-utils-workspace/work/` + ``` + ./cpd-cli manage update-custom-application-proxy-config \ + --instance_ns=zen \ + --app_name=code-engine-app \ + --app_run_id= \ + --app_proxy_config_yaml=/tmp/work/code-engine-app.conf.yaml + ``` + +7. Check the application running at `https://zen-route/physical_location//-/` diff --git a/code-engine-app/code-engine-app.conf.yaml b/code-engine-app/code-engine-app.conf.yaml new file mode 100644 index 0000000..2061d19 --- /dev/null +++ b/code-engine-app/code-engine-app.conf.yaml @@ -0,0 +1,37 @@ +apiVersion: zen.cpd.ibm.com/v1 +kind: ZenExtension +metadata: + name: {{ .serviceName }} +spec: + extensions: | + [ + { + "extension_name": "{{ .serviceName }}", + "extension_type": "nginx", + "details": { + "upstream_conf": "upstream.conf", + "location_conf": "nginx.conf" + } + } + ] + upstream.conf: | + upstream {{ .upstreamName }} { + keepalive 32; + keepalive_timeout 30s; + keepalive_requests 500; + server {{ .serviceName }}.{{ .dpPhyLocWorkloadNs }}.svc:{{ .servicePort }}; + } + nginx.conf: | + location ~* /{{ .serviceName }}/(.*) { + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Connection ""; + proxy_ssl_server_name on; + proxy_buffering off; + proxy_set_header If-Modified-Since ""; + proxy_set_header If-None-Match ""; + proxy_pass http://{{ .upstreamName }}/$1$is_args$args; + proxy_pass_header X-Accel-Buffering; + sub_filter_once off; + sub_filter '/info/' '/physical_location/{{ .zenPhysicalLocationName }}/{{ .serviceName }}/info/'; + } diff --git a/code-engine-app/code-engine-handson-sample-app/.dockerignore b/code-engine-app/code-engine-handson-sample-app/.dockerignore new file mode 100644 index 0000000..40b878d --- /dev/null +++ b/code-engine-app/code-engine-handson-sample-app/.dockerignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/code-engine-app/code-engine-handson-sample-app/.gitignore b/code-engine-app/code-engine-handson-sample-app/.gitignore new file mode 100644 index 0000000..6704566 --- /dev/null +++ b/code-engine-app/code-engine-handson-sample-app/.gitignore @@ -0,0 +1,104 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/code-engine-app/code-engine-handson-sample-app/Dockerfile b/code-engine-app/code-engine-handson-sample-app/Dockerfile new file mode 100644 index 0000000..acd7d71 --- /dev/null +++ b/code-engine-app/code-engine-handson-sample-app/Dockerfile @@ -0,0 +1,12 @@ +FROM node:17.4 + +RUN apt-get -y update && apt-get -y upgrade + +RUN apt-get remove -y --purge 'mysql-.*' + +RUN mkdir -p /usr/src/app +WORKDIR /usr/src/app +COPY . . +RUN echo "now building..." +RUN npm install +CMD [ "npm", "start"] diff --git a/code-engine-app/code-engine-handson-sample-app/README.md b/code-engine-app/code-engine-handson-sample-app/README.md new file mode 100644 index 0000000..4bd9837 --- /dev/null +++ b/code-engine-app/code-engine-handson-sample-app/README.md @@ -0,0 +1,14 @@ +# code-engine-handson-sample-app + +This is a sample application for Code Engine Hanson. + + +### Environment Params + +* BUCKETNAME : Bucket Name of IBM Object Storage +* TESTVALUE : A Config map Value +* COS_ENDPOINT : ENDPOINT of IBM Object Storage +* COS_APIKEY : API Key of IBM Object Storage +* COS_RESOURCE_INSTANCE_ID : resource instance ID of IBM Object Storage + + diff --git a/code-engine-app/code-engine-handson-sample-app/app.js b/code-engine-app/code-engine-handson-sample-app/app.js new file mode 100644 index 0000000..f4b2062 --- /dev/null +++ b/code-engine-app/code-engine-handson-sample-app/app.js @@ -0,0 +1,39 @@ +var createError = require('http-errors'); +var express = require('express'); +var path = require('path'); +var cookieParser = require('cookie-parser'); +var logger = require('morgan'); + +var indexRouter = require('./routes/index'); +var infoRouter = require('./routes/info'); + +var app = express(); + +// view engine setup + +app.use(logger('dev')); +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); +app.set("view engine", "html"); +app.use('/', indexRouter); +app.use('/info', infoRouter); + +// catch 404 and forward to error handler +app.use(function(req, res, next) { + next(createError(404)); +}); + +// error handler +app.use(function(err, req, res, next) { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get('env') === 'development' ? err : {}; + + // render the error page + res.status(err.status || 500); + res.render('error'); +}); + +module.exports = app; diff --git a/code-engine-app/code-engine-handson-sample-app/bin/www b/code-engine-app/code-engine-handson-sample-app/bin/www new file mode 100755 index 0000000..2710236 --- /dev/null +++ b/code-engine-app/code-engine-handson-sample-app/bin/www @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var app = require('../app'); +var debug = require('debug')('code-engine-handson:server'); +var http = require('http'); + +/** + * Get port from environment and store in Express. + */ + +var port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +var server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + var port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + var bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); +} diff --git a/code-engine-app/code-engine-handson-sample-app/package-lock.json b/code-engine-app/code-engine-handson-sample-app/package-lock.json new file mode 100644 index 0000000..6825345 --- /dev/null +++ b/code-engine-app/code-engine-handson-sample-app/package-lock.json @@ -0,0 +1,2795 @@ +{ + "name": "code-engine-handson-sample-app", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "code-engine-handson-sample-app", + "version": "0.0.0", + "dependencies": { + "cookie-parser": "~1.4.4", + "debug": "~2.6.9", + "express": "~4.16.1", + "http-errors": "~1.6.3", + "ibm-cos-sdk": "^1.11.0", + "jade": "~1.11.0", + "morgan": "~1.9.1" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", + "integrity": "sha512-pXK8ez/pVjqFdAgBkF1YPVRacuLQ9EXBKaKWaeh58WNfMkCmZhOZzu+NtKSPD5PHmCCHheQ5cD29qM1K4QTxIg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz", + "integrity": "sha512-j3/4pkfih8W4NK22gxVSXcEonTpAHOHh0hu5BoZrKcOsW/4oBPxTi4Yk3SAj+FhC1f3+bRTkXdm4019gw1vg9g==", + "dependencies": { + "acorn": "^2.1.0" + } + }, + "node_modules/align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha512-GrTZLRpmp6wIC2ztrWW9MjjTgSKccffgFagbNDOX95/dcjEcYZibYTeaOntySQLcdw1ztBoFkviiUvTMbb9MYg==", + "dependencies": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==", + "engines": { + "node": ">=0.4.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/asap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/asap/-/asap-1.0.0.tgz", + "integrity": "sha512-Ej9qjcXY+8Tuy1cNqiwNMwFRXOy9UwgTeMA8LxreodygIPV48lx8PU1ecFxb5ZeU1DpMKxiq6vGLTxcitWZPbA==" + }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/asn1.js/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, + "node_modules/body-parser": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha512-YQyoqQG3sO8iCmf8+hyVpgHHOv0/hCEFiS4zTGUwTA1HjAFX66wRcNQrVCeJq9pgESMRvUAOvSil5MJlmccuKQ==", + "dependencies": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "dependencies": { + "bn.js": "^5.0.0", + "randombytes": "^2.0.1" + } + }, + "node_modules/browserify-sign": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", + "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "dependencies": { + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.3", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + } + }, + "node_modules/browserify-sign/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/browserify-sign/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==" + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha512-Baz3aNe2gd2LP2qk5U+sDk/m4oSuwSDcBfayTCTBoWpfIGO5XFxPmjILQII4NGiZjD6DoDI6kf7gKaxkf7s3VQ==", + "dependencies": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/character-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-1.2.1.tgz", + "integrity": "sha512-6OEBVBlf/y8LaAphnbAnt743O3zMhlBer+FO5D40H6wqAdU9B1TvuApkejgLW0cvv0tEZNLktv1AnRI+C87ueQ==" + }, + "node_modules/cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/clean-css": { + "version": "3.4.28", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-3.4.28.tgz", + "integrity": "sha512-aTWyttSdI2mYi07kWqHi24NUU9YlELFKGOAgFzZjDN1064DMAOy2FBuoyGmkKRlXkbpXd0EVHmiVkbKhKoirTw==", + "dependencies": { + "commander": "2.8.x", + "source-map": "0.4.x" + }, + "bin": { + "cleancss": "bin/cleancss" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-css/node_modules/commander": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", + "integrity": "sha512-+pJLBFVk+9ZZdlAOB5WuIElVPPth47hILFkmGym57aq8kwxsowvByvB0DHs1vQAhyMZzdcpTtF0VDKGkSDR4ZQ==", + "dependencies": { + "graceful-readlink": ">= 1.0.0" + }, + "engines": { + "node": ">= 0.6.x" + } + }, + "node_modules/cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha512-GIOYRizG+TGoc7Wgc1LiOTLare95R3mzKgoln+Q/lE4ceiYH19gUpl0l0Ffq4lJDEf3FxujMe6IBfOCs7pfqNA==", + "dependencies": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + } + }, + "node_modules/commander": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", + "integrity": "sha512-PhbTMT+ilDXZKqH8xbvuUY2ZEQNef0Q7DKxgoEKb4ccytsdvVVJmYqR0sGbi96nxU6oGrwEIQnclpK2NBZuQlg==", + "engines": { + "node": ">= 0.6.x" + } + }, + "node_modules/constantinople": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.0.2.tgz", + "integrity": "sha512-UnEggAQrmhxuTxlb7n1OsTtagNXWUv2CRlOogZhWOU4jLK4EJEbF8UDSNxuGu+jVtWNtO2j51ab2H1wlBIzF/w==", + "deprecated": "Please update to at least constantinople 3.1.1", + "dependencies": { + "acorn": "^2.1.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "node_modules/create-ecdh/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dependencies": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + }, + "engines": { + "node": "*" + } + }, + "node_modules/css": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/css/-/css-1.0.8.tgz", + "integrity": "sha512-qmTYWhHk910nQWnGqMAiWWPQlB6tESiWgNebQJmiozOAGcBAQ1+U/UzUOkhdrcshlkSRRiKWodwmVvO0OmnIGg==", + "dependencies": { + "css-parse": "1.0.4", + "css-stringify": "1.0.5" + } + }, + "node_modules/css-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.0.4.tgz", + "integrity": "sha512-pfstzKVRZiHprDXdsmtfH1HYUEw22lzjuHdnpe1hscwoQvgW2C5zDQIBE0RKoALEReTn9W1ECdY8uaT/kO4VfA==" + }, + "node_modules/css-stringify": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/css-stringify/-/css-stringify-1.0.5.tgz", + "integrity": "sha512-aIThpcErhG5EyHorGqNlTh0TduNBqLrrXLO3x5rku3ZKBxuVfY+T7noyM2G2X/01iQANqJUb6d3+FLoa+N7Xwg==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/des.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", + "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==" + }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/diffie-hellman/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, + "node_modules/elliptic/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/express": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", + "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", + "dependencies": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.3", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.4", + "qs": "6.5.2", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.2", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==" + }, + "node_modules/hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/hash-base/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/hash-base/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ibm-cos-sdk": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/ibm-cos-sdk/-/ibm-cos-sdk-1.11.0.tgz", + "integrity": "sha512-6Z3PKCwVdw9ErklSfhlUV40gPN+yCGkZGlPAnLQuCU+NTiDfQlLgRpOjusGMacURaBamVHzuBuXd8v6CJ0DjRQ==", + "dependencies": { + "buffer": "^4.9.2", + "crypto-browserify": "^3.12.0", + "jmespath": "^0.15.0", + "url": "^0.10.3", + "uuid": "^3.4.0", + "xml2js": "^0.4.23", + "xmlbuilder": "^10.1.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/jade": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/jade/-/jade-1.11.0.tgz", + "integrity": "sha512-J76sbGKeLtu7uwW97Ntzb1UvGnpKTDplYa9ROr2gNRhM+SxvlBSG0Ees3TQ8+7ya2UVkzMEeFxhRhEpN68s7Tg==", + "deprecated": "Jade has been renamed to pug, please install the latest version of pug instead of jade", + "dependencies": { + "character-parser": "1.2.1", + "clean-css": "^3.1.9", + "commander": "~2.6.0", + "constantinople": "~3.0.1", + "jstransformer": "0.0.2", + "mkdirp": "~0.5.0", + "transformers": "2.1.0", + "uglify-js": "^2.4.19", + "void-elements": "~2.0.1", + "with": "~4.0.0" + }, + "bin": { + "jade": "bin/jade.js" + } + }, + "node_modules/jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha512-+kHj8HXArPfpPEKGLZ+kB5ONRTCiGQXo8RQYL0hH8t6pWXUBBK5KkkQmTNOwKK4LEsd0yTsgtjJVm4UBSZea4w==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/jstransformer": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-0.0.2.tgz", + "integrity": "sha512-b7tmf91j1ChMuYhwbPBnNgB62dmHuqiHpOdd6QLKzde8HydZqm+ud3qWreGWecSxPBFFNOf1Ozjx0xo2plFdHA==", + "dependencies": { + "is-promise": "^2.0.0", + "promise": "^6.0.1" + } + }, + "node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/miller-rabin/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, + "node_modules/mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "bin": { + "mime": "cli.js" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/morgan": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", + "integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==", + "dependencies": { + "basic-auth": "~2.0.0", + "debug": "2.6.9", + "depd": "~1.1.2", + "on-finished": "~2.3.0", + "on-headers": "~1.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/optimist": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", + "integrity": "sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ==", + "dependencies": { + "wordwrap": "~0.0.2" + } + }, + "node_modules/parse-asn1": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "dependencies": { + "asn1.js": "^5.2.0", + "browserify-aes": "^1.0.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/pbkdf2": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "dependencies": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/promise": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-6.1.0.tgz", + "integrity": "sha512-O+uwGKreKNKkshzZv2P7N64lk6EP17iXBn0PbUnNQhk+Q0AHLstiTrjkx3v5YBd3cxUe7Sq6KyRhl/A0xUjk7Q==", + "dependencies": { + "asap": "~1.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/public-encrypt/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, + "node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + }, + "node_modules/qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "dependencies": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha512-yqINtL/G7vs2v+dFIZmFUDbnVyFUJFKd6gK22Kgo6R4jfJGFtisKyncWDDULgjfqf4ASQuIQyjJ7XZ+3aWpsAg==", + "dependencies": { + "align-text": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "node_modules/send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "node_modules/source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha512-Y8nIfcb1s/7DcobUz1yOO1GSp7gyL+D9zLHDehT7iRESqGSxjJ448Sg7rvfgsRJCnKLdSl11uGf0s9X80cH0/A==", + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/transformers": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/transformers/-/transformers-2.1.0.tgz", + "integrity": "sha1-XSPLNVYd2F3Gf7hIIwm0fVPM6ac=", + "deprecated": "Deprecated, use jstransformer", + "dependencies": { + "css": "~1.0.8", + "promise": "~2.0", + "uglify-js": "~2.2.5" + } + }, + "node_modules/transformers/node_modules/is-promise": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz", + "integrity": "sha512-mjWH5XxnhMA8cFnDchr6qRP9S/kLntKuEfIYku+PaN1CnS8v+OG9O/BKpRCVRJvpIkgAZm0Pf5Is3iSSOILlcg==" + }, + "node_modules/transformers/node_modules/promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-2.0.0.tgz", + "integrity": "sha512-OgMc+sxI3zWF8D5BJGtA0z7/IsrDy1/0cPaDv6HPpqa2fSTo7AdON5U10NbZCUeF+zCAj3PtfPE50Hf02386aA==", + "dependencies": { + "is-promise": "~1" + } + }, + "node_modules/transformers/node_modules/source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ==", + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/transformers/node_modules/uglify-js": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.2.5.tgz", + "integrity": "sha1-puAqcNg5eSuXgEiLe4sYTAlcmcc=", + "dependencies": { + "optimist": "~0.3.5", + "source-map": "~0.1.7" + }, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dependencies": { + "source-map": "~0.5.1", + "yargs": "~3.10.0" + }, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + }, + "optionalDependencies": { + "uglify-to-browserify": "~1.0.0" + } + }, + "node_modules/uglify-js/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "optional": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/with": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/with/-/with-4.0.3.tgz", + "integrity": "sha1-7v0VTp550sjTQXtkeo8U2f7M4U4=", + "dependencies": { + "acorn": "^1.0.1", + "acorn-globals": "^1.0.3" + } + }, + "node_modules/with/node_modules/acorn": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-1.2.2.tgz", + "integrity": "sha512-FsqWmApWGMGLKKNpHt12PMc5AK7BaZee0WRh04fCysmTzHe+rrKOa2MKjORhnzfpe4r0JnfdqHn02iDA9Dqj2A==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dependencies": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + }, + "dependencies": { + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", + "integrity": "sha512-pXK8ez/pVjqFdAgBkF1YPVRacuLQ9EXBKaKWaeh58WNfMkCmZhOZzu+NtKSPD5PHmCCHheQ5cD29qM1K4QTxIg==" + }, + "acorn-globals": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz", + "integrity": "sha512-j3/4pkfih8W4NK22gxVSXcEonTpAHOHh0hu5BoZrKcOsW/4oBPxTi4Yk3SAj+FhC1f3+bRTkXdm4019gw1vg9g==", + "requires": { + "acorn": "^2.1.0" + } + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha512-GrTZLRpmp6wIC2ztrWW9MjjTgSKccffgFagbNDOX95/dcjEcYZibYTeaOntySQLcdw1ztBoFkviiUvTMbb9MYg==", + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==" + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "asap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/asap/-/asap-1.0.0.tgz", + "integrity": "sha512-Ej9qjcXY+8Tuy1cNqiwNMwFRXOy9UwgTeMA8LxreodygIPV48lx8PU1ecFxb5ZeU1DpMKxiq6vGLTxcitWZPbA==" + }, + "asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, + "body-parser": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha512-YQyoqQG3sO8iCmf8+hyVpgHHOv0/hCEFiS4zTGUwTA1HjAFX66wRcNQrVCeJq9pgESMRvUAOvSil5MJlmccuKQ==", + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "requires": { + "bn.js": "^5.0.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", + "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "requires": { + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.3", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==" + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==" + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==" + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha512-Baz3aNe2gd2LP2qk5U+sDk/m4oSuwSDcBfayTCTBoWpfIGO5XFxPmjILQII4NGiZjD6DoDI6kf7gKaxkf7s3VQ==", + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "character-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-1.2.1.tgz", + "integrity": "sha512-6OEBVBlf/y8LaAphnbAnt743O3zMhlBer+FO5D40H6wqAdU9B1TvuApkejgLW0cvv0tEZNLktv1AnRI+C87ueQ==" + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "clean-css": { + "version": "3.4.28", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-3.4.28.tgz", + "integrity": "sha512-aTWyttSdI2mYi07kWqHi24NUU9YlELFKGOAgFzZjDN1064DMAOy2FBuoyGmkKRlXkbpXd0EVHmiVkbKhKoirTw==", + "requires": { + "commander": "2.8.x", + "source-map": "0.4.x" + }, + "dependencies": { + "commander": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", + "integrity": "sha512-+pJLBFVk+9ZZdlAOB5WuIElVPPth47hILFkmGym57aq8kwxsowvByvB0DHs1vQAhyMZzdcpTtF0VDKGkSDR4ZQ==", + "requires": { + "graceful-readlink": ">= 1.0.0" + } + } + } + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha512-GIOYRizG+TGoc7Wgc1LiOTLare95R3mzKgoln+Q/lE4ceiYH19gUpl0l0Ffq4lJDEf3FxujMe6IBfOCs7pfqNA==", + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + } + }, + "commander": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", + "integrity": "sha512-PhbTMT+ilDXZKqH8xbvuUY2ZEQNef0Q7DKxgoEKb4ccytsdvVVJmYqR0sGbi96nxU6oGrwEIQnclpK2NBZuQlg==" + }, + "constantinople": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.0.2.tgz", + "integrity": "sha512-UnEggAQrmhxuTxlb7n1OsTtagNXWUv2CRlOogZhWOU4jLK4EJEbF8UDSNxuGu+jVtWNtO2j51ab2H1wlBIzF/w==", + "requires": { + "acorn": "^2.1.0" + } + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + }, + "cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "requires": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + } + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "css": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/css/-/css-1.0.8.tgz", + "integrity": "sha512-qmTYWhHk910nQWnGqMAiWWPQlB6tESiWgNebQJmiozOAGcBAQ1+U/UzUOkhdrcshlkSRRiKWodwmVvO0OmnIGg==", + "requires": { + "css-parse": "1.0.4", + "css-stringify": "1.0.5" + } + }, + "css-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.0.4.tgz", + "integrity": "sha512-pfstzKVRZiHprDXdsmtfH1HYUEw22lzjuHdnpe1hscwoQvgW2C5zDQIBE0RKoALEReTn9W1ECdY8uaT/kO4VfA==" + }, + "css-stringify": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/css-stringify/-/css-stringify-1.0.5.tgz", + "integrity": "sha512-aIThpcErhG5EyHorGqNlTh0TduNBqLrrXLO3x5rku3ZKBxuVfY+T7noyM2G2X/01iQANqJUb6d3+FLoa+N7Xwg==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==" + }, + "des.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", + "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==" + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + } + } + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "express": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", + "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", + "requires": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.3", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.4", + "qs": "6.5.2", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.2", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==" + } + } + }, + "finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==" + }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "ibm-cos-sdk": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/ibm-cos-sdk/-/ibm-cos-sdk-1.11.0.tgz", + "integrity": "sha512-6Z3PKCwVdw9ErklSfhlUV40gPN+yCGkZGlPAnLQuCU+NTiDfQlLgRpOjusGMacURaBamVHzuBuXd8v6CJ0DjRQ==", + "requires": { + "buffer": "^4.9.2", + "crypto-browserify": "^3.12.0", + "jmespath": "^0.15.0", + "url": "^0.10.3", + "uuid": "^3.4.0", + "xml2js": "^0.4.23", + "xmlbuilder": "^10.1.1" + } + }, + "iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "jade": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/jade/-/jade-1.11.0.tgz", + "integrity": "sha512-J76sbGKeLtu7uwW97Ntzb1UvGnpKTDplYa9ROr2gNRhM+SxvlBSG0Ees3TQ8+7ya2UVkzMEeFxhRhEpN68s7Tg==", + "requires": { + "character-parser": "1.2.1", + "clean-css": "^3.1.9", + "commander": "~2.6.0", + "constantinople": "~3.0.1", + "jstransformer": "0.0.2", + "mkdirp": "~0.5.0", + "transformers": "2.1.0", + "uglify-js": "^2.4.19", + "void-elements": "~2.0.1", + "with": "~4.0.0" + } + }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha512-+kHj8HXArPfpPEKGLZ+kB5ONRTCiGQXo8RQYL0hH8t6pWXUBBK5KkkQmTNOwKK4LEsd0yTsgtjJVm4UBSZea4w==" + }, + "jstransformer": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-0.0.2.tgz", + "integrity": "sha512-b7tmf91j1ChMuYhwbPBnNgB62dmHuqiHpOdd6QLKzde8HydZqm+ud3qWreGWecSxPBFFNOf1Ozjx0xo2plFdHA==", + "requires": { + "is-promise": "^2.0.0", + "promise": "^6.0.1" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "requires": { + "is-buffer": "^1.1.5" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==" + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg==" + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "requires": { + "minimist": "^1.2.6" + } + }, + "morgan": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", + "integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==", + "requires": { + "basic-auth": "~2.0.0", + "debug": "2.6.9", + "depd": "~1.1.2", + "on-finished": "~2.3.0", + "on-headers": "~1.0.1" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "optimist": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", + "integrity": "sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ==", + "requires": { + "wordwrap": "~0.0.2" + } + }, + "parse-asn1": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "requires": { + "asn1.js": "^5.2.0", + "browserify-aes": "^1.0.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "pbkdf2": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "promise": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-6.1.0.tgz", + "integrity": "sha512-O+uwGKreKNKkshzZv2P7N64lk6EP17iXBn0PbUnNQhk+Q0AHLstiTrjkx3v5YBd3cxUe7Sq6KyRhl/A0xUjk7Q==", + "requires": { + "asap": "~1.0.0" + } + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==" + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==" + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha512-yqINtL/G7vs2v+dFIZmFUDbnVyFUJFKd6gK22Kgo6R4jfJGFtisKyncWDDULgjfqf4ASQuIQyjJ7XZ+3aWpsAg==", + "requires": { + "align-text": "^0.1.1" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + } + }, + "serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha512-Y8nIfcb1s/7DcobUz1yOO1GSp7gyL+D9zLHDehT7iRESqGSxjJ448Sg7rvfgsRJCnKLdSl11uGf0s9X80cH0/A==", + "requires": { + "amdefine": ">=0.0.4" + } + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, + "transformers": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/transformers/-/transformers-2.1.0.tgz", + "integrity": "sha1-XSPLNVYd2F3Gf7hIIwm0fVPM6ac=", + "requires": { + "css": "~1.0.8", + "promise": "~2.0", + "uglify-js": "~2.2.5" + }, + "dependencies": { + "is-promise": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz", + "integrity": "sha512-mjWH5XxnhMA8cFnDchr6qRP9S/kLntKuEfIYku+PaN1CnS8v+OG9O/BKpRCVRJvpIkgAZm0Pf5Is3iSSOILlcg==" + }, + "promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-2.0.0.tgz", + "integrity": "sha512-OgMc+sxI3zWF8D5BJGtA0z7/IsrDy1/0cPaDv6HPpqa2fSTo7AdON5U10NbZCUeF+zCAj3PtfPE50Hf02386aA==", + "requires": { + "is-promise": "~1" + } + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ==", + "requires": { + "amdefine": ">=0.0.4" + } + }, + "uglify-js": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.2.5.tgz", + "integrity": "sha1-puAqcNg5eSuXgEiLe4sYTAlcmcc=", + "requires": { + "optimist": "~0.3.5", + "source-map": "~0.1.7" + } + } + } + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==" + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "optional": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + }, + "with": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/with/-/with-4.0.3.tgz", + "integrity": "sha1-7v0VTp550sjTQXtkeo8U2f7M4U4=", + "requires": { + "acorn": "^1.0.1", + "acorn-globals": "^1.0.3" + }, + "dependencies": { + "acorn": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-1.2.2.tgz", + "integrity": "sha512-FsqWmApWGMGLKKNpHt12PMc5AK7BaZee0WRh04fCysmTzHe+rrKOa2MKjORhnzfpe4r0JnfdqHn02iDA9Dqj2A==" + } + } + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" + }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "dependencies": { + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + } + } + }, + "xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==" + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } +} diff --git a/code-engine-app/code-engine-handson-sample-app/package.json b/code-engine-app/code-engine-handson-sample-app/package.json new file mode 100644 index 0000000..7be2ff0 --- /dev/null +++ b/code-engine-app/code-engine-handson-sample-app/package.json @@ -0,0 +1,17 @@ +{ + "name": "code-engine-handson-sample-app", + "version": "0.0.0", + "private": true, + "scripts": { + "start": "node ./bin/www" + }, + "dependencies": { + "cookie-parser": "~1.4.4", + "debug": "~2.6.9", + "express": "~4.16.1", + "http-errors": "~1.6.3", + "ibm-cos-sdk": "^1.11.0", + "jade": "~1.11.0", + "morgan": "~1.9.1" + } +} diff --git a/code-engine-app/code-engine-handson-sample-app/public/index.html b/code-engine-app/code-engine-handson-sample-app/public/index.html new file mode 100644 index 0000000..0f45c6d --- /dev/null +++ b/code-engine-app/code-engine-handson-sample-app/public/index.html @@ -0,0 +1,46 @@ + + + + Code Engine勉強会 + + + +
+      ___  __  ____  ____    ____  __ _   ___  __  __ _  ____  ____  ____  ____  ____  __  __   __ _ 
+ / __)/  \(    \(  __)  (  __)(  ( \ / __)(  )(  ( \(  __)/ ___)(  __)/ ___)/ ___)(  )/  \ (  ( \
+( (__(  O )) D ( ) _)    ) _) /    /( (_ \ )( /    / ) _) \___ \ ) _) \___ \\___ \ )((  O )/    /
+ \___)\__/(____/(____)  (____)\_)__) \___/(__)\_)__)(____)(____/(____)(____/(____/(__)\__/ \_)__)
+
+ 
Pod Name :
+
Configmap Value :
+
Bucket Name From Secret:
+ + +
+ +
+ + + \ No newline at end of file diff --git a/code-engine-app/code-engine-handson-sample-app/public/stylesheets/style.css b/code-engine-app/code-engine-handson-sample-app/public/stylesheets/style.css new file mode 100644 index 0000000..9453385 --- /dev/null +++ b/code-engine-app/code-engine-handson-sample-app/public/stylesheets/style.css @@ -0,0 +1,8 @@ +body { + padding: 50px; + font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; +} + +a { + color: #00B7FF; +} diff --git a/code-engine-app/code-engine-handson-sample-app/routes/index.js b/code-engine-app/code-engine-handson-sample-app/routes/index.js new file mode 100644 index 0000000..9e9cdc8 --- /dev/null +++ b/code-engine-app/code-engine-handson-sample-app/routes/index.js @@ -0,0 +1,11 @@ +var express = require('express'); +var router = express.Router(); + +/* GET home page. */ +router.get('/get', function(req, res, next) { + res.render('index', { title: 'Express' }); +}); + + + +module.exports = router; diff --git a/code-engine-app/code-engine-handson-sample-app/routes/info.js b/code-engine-app/code-engine-handson-sample-app/routes/info.js new file mode 100644 index 0000000..3422a90 --- /dev/null +++ b/code-engine-app/code-engine-handson-sample-app/routes/info.js @@ -0,0 +1,48 @@ +var express = require('express'); +var router = express.Router(); +var icos = require("../service/objectstorage"); + +/* GET users listing. */ +router.get('/get', function(req, res, next) { + var podName = process.env.HOSTNAME || "pod Name desu"; + var configmapValue = process.env.TESTVALUE || "config Map Value desu"; + var bucketName = process.env.BUCKETNAME || "bucket Name desu"; + + res.json({ + "podName" : podName, + "configmapValue" : configmapValue, + "bucketName" : bucketName + }); +}); + +router.post('/post',async (req,res) => { + + var podName = process.env.HOSTNAME || "pod Name desu"; + var configmapValue = process.env.TESTVALUE || "configMapValuedesu"; + var bucketName = process.env.BUCKETNAME || "bucket Name desu"; + + var json = { + "podName" : podName, + "configmapValue" : configmapValue, + "bucketName" : bucketName + }; + + var key = "code-engine-session-"+podName+"_"+configmapValue+".json"; + + try { + await icos.uploadP3Data(key,json); + res.json({ + "status" : 200, + "message" : "ICOSへのUploadが完了しました。" + }) + } catch (e) { + res.json({ + "status" : 503, + "message" : "ICOSへのUploadが失敗しました。" + }) + } + + +}); + +module.exports = router; diff --git a/code-engine-app/code-engine-handson-sample-app/service/objectstorage.js b/code-engine-app/code-engine-handson-sample-app/service/objectstorage.js new file mode 100644 index 0000000..1338539 --- /dev/null +++ b/code-engine-app/code-engine-handson-sample-app/service/objectstorage.js @@ -0,0 +1,47 @@ +const ibm = require('ibm-cos-sdk'); + +const config = { + endpoint: process.env.COS_ENDPOINT, + apiKeyId: process.env.COS_APIKEY, + serviceInstanceId: process.env.COS_RESOURCE_INSTANCE_ID, +}; + +const cos = new ibm.S3(config); + +const bucketName = process.env.BUCKETNAME; + +exports.uploadP3Data = async (key, file) => { + + try { + + return await uploadObject(bucketName, key, JSON.stringify(file)); + } catch (e) { + console.log(e); + throw e; + } + + +} + + +var uploadObject = (bucketName, key, file) => { + + try { + + return cos.putObject({ + "Bucket": bucketName, + "Key": key, + "Body": file + }).promise() + .then(() => { + console.log(`Item: ${key} created!`); + }) + .catch((e) => { + console.log(`ERROR: ${e.code} - ${e.message}\n`); + }); + } catch (e) { + console.log(e); + throw e; + } + +} diff --git a/hello-world/Dockerfile b/hello-world/Dockerfile new file mode 100644 index 0000000..b963607 --- /dev/null +++ b/hello-world/Dockerfile @@ -0,0 +1,14 @@ +FROM registry.access.redhat.com/ubi9/ubi-minimal +USER 0 +RUN microdnf install python3 python3-pip -y \ + && pip3 install flask \ + && microdnf clean all +WORKDIR /app +COPY app.py . +COPY cert.pem . +COPY key.pem . +RUN chmod 644 cert.pem key.pem +RUN chmod 666 app.py +USER 1001 + +CMD ["python3", "app.py"] diff --git a/hello-world/README.md b/hello-world/README.md new file mode 100644 index 0000000..594e75e --- /dev/null +++ b/hello-world/README.md @@ -0,0 +1,24 @@ +# Deploy hello-world application to software hub dataplane + +hello-world is an api endpoint that would respond with "hello-world" when invoked from software hub default dataplane + +Checkout the [pre-requisites](../README.md#pre-requisites-to-deploy-sample-applications) required before deploying this application. + +#### deploy using cpd-cli +``` +./cpd-cli manage create-dockerfile-application --instance_ns=zen \ + --app_name=hello-world \ + --app_port=8443 \ + --repo_url=https://github.com/IBM/swhub-custom-app-samples.git \ + --repo_token= \ + --repo_app_dir=hello-world \ + --cpu=400m \ + --memory=200Mi \ + --cpu_limit=500m \ + --memory_limit=400Mi \ + --tls_enabled=true + + command returns application run id +``` + +* application available at `curl -k https:///physical_location//-/hello-world` diff --git a/hello-world/app.py b/hello-world/app.py new file mode 100644 index 0000000..d432deb --- /dev/null +++ b/hello-world/app.py @@ -0,0 +1,21 @@ +from flask import Flask +import ssl + +app = Flask(__name__) + +@app.route('/hello-world') +def hello_world(): + return 'hello world!' + +@app.route('/live') +def live(): + return 'live!' + +@app.route('/ready') +def ready(): + return 'ready!' + +if __name__ == '__main__': + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.load_cert_chain('cert.pem', 'key.pem') + app.run(host='0.0.0.0', port=8443, ssl_context=context, debug=True) diff --git a/hello-world/cert.pem b/hello-world/cert.pem new file mode 100644 index 0000000..9951c0e --- /dev/null +++ b/hello-world/cert.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCTCCAvGgAwIBAgIUTKnOMSDw0bLEliMduF/7v0eXKUQwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDYxMTE4MDAwN1oXDTI2MDYx +MTE4MDAwN1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA0BL37C1fb3pQc3yAsYF+bHDZopfJo1s2qFrdhmQjdjIj +mfCJqOvAnP+hwgPr9QCCtV9z02wGTwfKPW8ZiU4dtbzWPDM/pN772b8UOBFWpY9F +rDO1XF1/GRXDY0ELENBjJgoFLpGUYgEZ9VWSLf1M1sTYrq064AVk7mHFoNmOpy9p +dicCoBJQyDlj5G7W2p1pZmGnLrYiRiwV31FCBKnNT1KPcuCe8kdwDNNVLry5R52e +Or3gUxI+yRNxGZGyi2IJpsdKZZY13Ecr7p3WVweDw9bH4TpxaU81Kr9eJPefIiHs +E6il7xvbGWM7LWnk1qNwSuA9abyErrFdngr2xL7dpgT0EF11cAWRuBGK2uoIgRAN +RF7YPmwq8Bgw9O7YJ2DlQjRUINtv65WX5RKLazDjn5JthDeysRcGOuKx1xqrskIP +DhE5pOdKNWg6kFdkTgTjqC4tNH+nGLnS6UD2r+WC+V+o9z/GGF425SW13DCDeZOl +KRFuX4ooG/Qch9+TswYTvoSR4yzXjAslHqFjnGjWT2ACmFjOkXqPkwNssJA9FaGL +mWcce1gSM4C0Q3arLszEeEFnQicN5u3WWCArmhfgM2Fx53GkRlKIT51EgAv92HhO +KQnXasB0JyD+VVF3vwGUEfYgYDpJuAamautfqhH9VVawvqYoPSEj4gvM9pqFX/kC +AwEAAaNTMFEwHQYDVR0OBBYEFFEmplLvxUkUaUwvbtzOLVEKe8ffMB8GA1UdIwQY +MBaAFFEmplLvxUkUaUwvbtzOLVEKe8ffMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggIBAG2kk9ltQM896F4BlKPBFnr23Fycy9VJJYZSwYWIlTqpnRgg +Yg/ihVCRMZ9NA0g+HDo8L8lGykhm7CDt7KMIBUgMyG/Qy2Y37tAig/AsccLGZtG+ +wqcnpOg3vD+bDvYPOaSeMLfUQH3ZK21JwLVV/QE+pSU9ZTAg/6qY59mkVxNl2GBq +35FqfEHYkaYlBudnQk5Nf6XSUzSt99I+3SLvABTAFdJLhLLqzNlZXyCPadEANTvx +vzra+QOXweYftSziIUYKzLfo1qiyc0+ti5vfv+y6pswZqX6/EUXvRgw089fO/aj7 +xi1vue0lmEvJZhDRt1e7kuKsKXx9AnHmsPH1Psdc6HcjMEQnSTt4RmIS/HukWtqO +M/CdJkArmfNSbjsT2pYm1NpXbAUexm+abG2jFQ+gCKHLXu+8wX5hpip13MF2RuO1 +zm4t16R6ddm8kbg8KocgESV5uQAUu3ruTLCfgULz23OUreuFcpJ+O+jAot1og1XW +b5dP19f37OE7kuZphNS8Y+Su172qnW61g/qm/6dAJc0pjbKynQmhUzCzLg5voTvY +wCm2QPyQfah4GW3UUiYN06g8Kbl7s+BW3T4m8rf7qRTXZKcI8MG8S+0PQU7T0HFu +50z0qpgc7RfFfAS2yrTcZx7ZmiQpblWo3QN67jm2xlungH9R1S0MLQ7geLQr +-----END CERTIFICATE----- diff --git a/hello-world/generate-certs.sh b/hello-world/generate-certs.sh new file mode 100644 index 0000000..f7b4bc8 --- /dev/null +++ b/hello-world/generate-certs.sh @@ -0,0 +1,2 @@ +#!/bin/sh +openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=localhost" \ No newline at end of file diff --git a/hello-world/key.pem b/hello-world/key.pem new file mode 100644 index 0000000..731ed98 --- /dev/null +++ b/hello-world/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDQEvfsLV9velBz +fICxgX5scNmil8mjWzaoWt2GZCN2MiOZ8Imo68Cc/6HCA+v1AIK1X3PTbAZPB8o9 +bxmJTh21vNY8Mz+k3vvZvxQ4EValj0WsM7VcXX8ZFcNjQQsQ0GMmCgUukZRiARn1 +VZIt/UzWxNiurTrgBWTuYcWg2Y6nL2l2JwKgElDIOWPkbtbanWlmYacutiJGLBXf +UUIEqc1PUo9y4J7yR3AM01UuvLlHnZ46veBTEj7JE3EZkbKLYgmmx0plljXcRyvu +ndZXB4PD1sfhOnFpTzUqv14k958iIewTqKXvG9sZYzstaeTWo3BK4D1pvISusV2e +CvbEvt2mBPQQXXVwBZG4EYra6giBEA1EXtg+bCrwGDD07tgnYOVCNFQg22/rlZfl +EotrMOOfkm2EN7KxFwY64rHXGquyQg8OETmk50o1aDqQV2ROBOOoLi00f6cYudLp +QPav5YL5X6j3P8YYXjblJbXcMIN5k6UpEW5fiigb9ByH35OzBhO+hJHjLNeMCyUe +oWOcaNZPYAKYWM6Reo+TA2ywkD0VoYuZZxx7WBIzgLRDdqsuzMR4QWdCJw3m7dZY +ICuaF+AzYXHncaRGUohPnUSAC/3YeE4pCddqwHQnIP5VUXe/AZQR9iBgOkm4BqZq +61+qEf1VVrC+pig9ISPiC8z2moVf+QIDAQABAoICAAQmKgPEyy84Y6v3MsPCCXN/ +yT8zl/rmFzrChmynHjPoVQsSn6lwJgjwv1jUkNseVaeTOIaMlBBLCfkxTXOOTLZd +MR0bMoIxoMOwn+ZsUMDVZgtvLoch0CZ/vMPZE1o++t3Se6mi5TAScIzoAMBiWYAQ +2yHvOgonNreFg0DYBb8HXtV9z/tsJ2iOsxNmMxTskvej1ocZF6mJjdgZpooxmUf4 +eqbM8YPIRo+Nk+3DfU2pyB1zatU35cDxs818ORthaurZ9NL+F03bV3YXIfkzS6ks +j74fLGLFXbyKpgXGWvR9yQItWJMM3aYYc0t5CJilGANONxCe3eXJNfeFpXJlPJsz +2abH0KHT90I03t7oA1edxiSzuvmYkZt+lE0kYPYagDKLrZhOb3ORwRXIre5FJ/Nt +6waDQQZ9bDqnzTxz9SJCoG9JFJUUFjEspg+ivrCcEOTlskhYHjPH5A/h6lYIvDJD +C6tmN7JPR4GZwb67jZ4cJqF9xHGdWynAySy+vmEvc4zKPB4C/FtlnizE6O4TkcwY +jeZ6SKD5njl/b27s2xyT3qfjMoTZEe0GFVpgIwrb3CwK16xx32zIBZintE03tMJV +mtAVmSjuB3Yze/rAHlExRyEWClIpglLIlQaNgKj15JN5Hqx+q+anHDpGDLhl3j62 +TCQnwFVltlUtiR0oH8ynAoIBAQDt6GwHECy4jeN33YpyIXoaoq6DENqwiTfdwQbl +6fM2A5ZWX2ftAg6Prl5L0+T3q71sUkXLkpml0jlFD4kGLrlpdd/gNXbD83ROh5gX +3og5q5WXfWmAFMvsCrZP65K7scMrzljbcjq0J3j7E2q5IFCljLlqnH7Yn0hv+4q2 +MuvtZHpgx/DDkWWKpo0E0Jwv93YzSzuWlchWWmBo5anYN0iaGU3RNULqtHZY8Vk5 +C0DkDbX3STwLtNLltEIFvFqjplWLeuvdW2OWYn2mQOjGVcLIWDHPmKF0y9lqYk++ ++Y2OfV0x/P7qkr8+r0kcT7L+PVbLG4W7Q4n6niFEcoAyGbsrAoIBAQDf5b5df1qg +J5zPiFWNPSEvg17err20aLwkaBVXbknzpVwY9SZdUdEp9YBK5yLjeL1xy6Bx5ZHS +B45LN35DttDdjjHpFYSwlx6Snfxuu/+QnRAW7dFiY/21wCV+oar56Kd6v4frPylA +UFogHoLXqmaEwW5/CJZ02d8/QPqQ0WXNnILVzw6h4VqEPCaBsqPsfy/zreuo1TBD +ep6CcjDgkLwnO8D8s0RkVYaQhe9V/zMcdNPJVyLDczMoFnYY8PHQPDWXDd1+jtjF +zOefJzKQT7VjG4niw7FlrnMVCOl9GVC3g3xgNCv+Tw1ZA+NME4BhjpSwcIjD4U7P +urLMUvscle9rAoIBAQCoqNY0562cpNJ2/rMQmFUVHvGy7zbqYk0dw6NdSU7nowFo +eDiYGJY0HDyzayPuxW0Dpwvd9y9AFFoPcKUGogdGEY/GRoDNCAITTiNwAI34vIDP +pdQuPhAXZby7ydj7gVlI6/+oi0G8yCMHdEt7tMIXqz0jrJBsh/d1lBFRe6YwZMQx +am1wZ9phwhNFL5MamkfMs3AqIEOnnGiu1Opk40RwIrI9N5IaDBe+lFNZknXNdz7i +caPE9MtylykbId3sGJaazGj8Q7bFPUuwmmGrgc8V8xhVAPKtd3rJ5ap9TDOjqZJx +Rtd9es1j5iZhkMrnXJr0YK2dBZOC/am9+aKSYhWDAoIBAFtcLd+M9fff8hak9PPa ++82dd3q6JuKU0iCw7/RrJnXrBbeRYQQ3PHg4mw71XcEgJX1nr6KKIIRpXODIVhdf +Y77kJO2NQEWmhG0jVjwBLnld/ZC9nfDeCEq/iw0u/stW6fAmc1nEvhpmL0Bn8s/5 +0jddjq77XBl2RahT5WpuZ3IM6T5BbSEVCZBn0vadZFI+W9y9HSoZQ8ZlJITp/5PC +u2Pu+AKlS8T9ORmD7XNT3RFBgqHnkmHaggdbkvj+aE2mI9/tYnIf68haojDJF9LX +E5bLR+pmG9733jG5Hz1StSMM3hWLdGAi9bla4sbKNlqYiYetg8EaEMSm1AqX2auC +at0CggEBAKceYGJHuvQFAG7x4D6a8BJksTqBG2BQhVFDkvxy+0ZpgiqKoAATdOkQ +Si8YFtQ4G2KNDslDiGQoOWSc7pD15TGXkkXkzBj5c/X2tD196DlLJ/4TOwPiAXHl +hzOL4zndaU5BXFQ/79LKwBZlIt2ggmE39inHaBhvUd9eDQsQ17HaDi3QL4WSx7xf +WScfGGomsj0U33H07o4lCyonkRcXE8GjF58028CkHIbQdWqrDnvICGWkUm5R9SKr +sadMd7122I6MfmKRIXlOk0rsqJB5CEPaUYXhm1eaijA4LanZQmZ/w+wQKtfuJ2aa ++vIERnMo8IfjAKfFQP4z4edLNrWbkB8= +-----END PRIVATE KEY----- diff --git a/hello-world/requirements.txt b/hello-world/requirements.txt new file mode 100644 index 0000000..0712a4b --- /dev/null +++ b/hello-world/requirements.txt @@ -0,0 +1 @@ +Flask \ No newline at end of file diff --git a/mcp-gateway-forge/README.md b/mcp-gateway-forge/README.md new file mode 100644 index 0000000..a5a44e9 --- /dev/null +++ b/mcp-gateway-forge/README.md @@ -0,0 +1,114 @@ +# deploy mcp-context-forge app to software hub dataplane + +[ContextForge MCP Gateway](https://github.com/IBM/mcp-context-forge) is a feature-rich gateway, proxy and MCP Registry that federates MCP and REST services - unifying discovery, auth, rate-limiting, observability, virtual servers, multi-transport protocols, and an optional Admin UI into one clean endpoint for your AI clients + +Checkout the [pre-requisites](../README.md#pre-requisites-to-deploy-sample-applications) required before deploying this application. + +### deploy mcp-context-forge with sql lite database: + +- download proxy-config-yaml file: + ``` + download ./mcp-gateway-forge.conf.yaml and move to cpd-cli-workspace/olm-utils-workspace/work/mcp-gateway-forge.conf.yaml + ``` + +- create app-envs-json-sqlite.json: + + ``` + $ cat cpd-cli-workspace/olm-utils-workspace/work/app-env-json.json + {"HOST":"0.0.0.0","JWT_SECRET_KEY":"my-test-key","BASIC_AUTH_USER":"admin@example.com","BASIC_AUTH_PASSWORD":"changeme","AUTH_REQUIRED":"true","DATABASE_URL":"sqlite:////data/mcp.db","SSL":"true","CERT_FILE":"/etc/certs/tls.crt","KEY_FILE":"/etc/certs/tls.key","MCPGATEWAY_UI_ENABLED":"true","MCPGATEWAY_ADMIN_API_ENABLED":"true"} + ``` + +- run cpd-cli create-dockerfile-application: + ``` + cpd-cli manage create-dockerfile-application --instance_ns=zen \ + --app_name=mcp-context-forge-sqlite \ + --app_port=4444 \ + --app_port_tls=true \ + --repo_url=https://github.com/IBM/mcp-context-forge.git \ + --repo_branch=main \ + --dockerfile=Containerfile \ + --app_envs_json=/tmp/work/app-envs-json-sqlite.json \ + --pvc_info={"size":"2Gi","mount_path":"/data"} \ + --cpu=400m \ + --memory=200Mi \ + --cpu_limit=500m \ + --memory_limit=400Mi + ``` + +- check that the pod is in running status: + ``` + mcp-context-forge-sqlite-tcv6isrcslka-57bcbb95b-dshrl 1/1 Running + ``` + +- update application proxy config - copy `mcp-gateway-forge.conf.yaml` in this folder to `cpd-cli-workspace/olm-utils-workspace/work/` + ``` + ./cpd-cli manage update-custom-application-proxy-config \ + --instance_ns=zen \ + --app_name= mcp-context-forge-sqlite \ + --app_run_id= \ + --app_proxy_config_yaml=mcp-gateway-forge.conf.yaml + ``` + +### deploy mcp-context-forge with postgres database: + + #### deploy postgresql + + - download postgresql template tar + ``` + download postgresql.template.tgz to cpd-cli-workspace/olm-utils-workspace/work/postgesql.template.tgz + ``` + + - run following cpd-cli command: + ``` + ./cpd-cli manage create-oc-template-application --instance_ns=zen \ + --app_name=postgresql-mcp-context-forge \ + --app_tar_file=/tmp/work/postgesql.template.tgz \ + --cpu=400m \ + --memory=200Mi \ + --cpu_limit=500m \ + --memory_limit=400Mi + ``` + + - check that the pod is in running state: + ``` + # oc get po -n wl | grep postgres + postgresql-mcp-context-forge-1-g4ld7 1/1 Running + ``` + + #### deploy mcp-context-forge + + - create app-envs-json.json: + ``` + $ cat cpd-cli-workspace/olm-utils-workspace/work/app-envs-json-postgresql.json + + {"HOST":"0.0.0.0","JWT_SECRET_KEY":"my-test-key","BASIC_AUTH_USER":"admin@example.com","BASIC_AUTH_PASSWORD":"changeme","AUTH_REQUIRED":"true","DATABASE_URL":"postgresql://postgres:secret@postgresql-mcp-context-forge:5432/mcp","SSL":"true","CERT_FILE":"/etc/certs/tls.crt","KEY_FILE":"/etc/certs/tls.key","MCPGATEWAY_UI_ENABLED":"true","MCPGATEWAY_ADMIN_API_ENABLED":"true"} + ``` + - run cpd-cli create-dockerfile-application command: + ``` + cpd-cli manage create-dockerfile-application --instance_ns=zen \ + --app_name=mcp-context-forge-postgresql \ + --app_port=4444 \ + --app_port_tls=true \ + --repo_url=https://github.com/IBM/mcp-context-forge.git \ + --repo_branch=main \ + --dockerfile=Containerfile \ + --app_envs_json=/tmp/work/app-envs-json-postgresql.json \ + --cpu=400m \ + --memory=200Mi \ + --cpu_limit=500m \ + --memory_limit=400Mi + ``` + - check that the pod is in running status: + ``` + mcp-context-forge-postgresql-78bib026k314-57bcbb95b-dshrl 1/1 Running + ``` + - update application proxy config - copy `mcp-gateway-forge.conf.yaml` in this folder to `cpd-cli-workspace/olm-utils-workspace/work/` + ``` + ./cpd-cli manage update-custom-application-proxy-config \ + --instance_ns=zen \ + --app_name= mcp-context-forge-postgresql \ + --app_run_id= \ + --app_proxy_config_yaml=mcp-gateway-forge.conf.yaml + ``` + + #### application available at `https://zen-route/physical_location/default-pl//admin` diff --git a/mcp-gateway-forge/mcp-gateway-forge.conf.yaml b/mcp-gateway-forge/mcp-gateway-forge.conf.yaml new file mode 100644 index 0000000..91cd28e --- /dev/null +++ b/mcp-gateway-forge/mcp-gateway-forge.conf.yaml @@ -0,0 +1,50 @@ +apiVersion: zen.cpd.ibm.com/v1 +kind: ZenExtension +metadata: + name: {{ .serviceName }} +spec: + extensions: | + [ + { + "extension_name": "{{ .serviceName }}", + "details": { + "upstream_conf": "upstream.conf", + "location_conf": "nginx.conf" + } + } + ] + upstream.conf: | + upstream {{ .upstreamName }} { + keepalive 32; + keepalive_timeout 30s; + keepalive_requests 500; + server {{ .serviceName }}.{{ .dpPhyLocWorkloadNs }}.svc:{{ .servicePort }}; + } + nginx.conf: | + location ~* /{{ .serviceName }}/(.*) { + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Connection ""; + proxy_ssl_server_name on; + proxy_buffering off; + proxy_pass {{ .serviceUrlScheme }}://{{ .upstreamName }}/$1$is_args$args; + proxy_pass_header X-Accel-Buffering; + ##preserve original host on app redirect + proxy_redirect ~^(https?://[^/]+)?/admin([#/]?.*)$ $scheme://$http_x_original_host/physical_location/{{ .zenPhysicalLocationName }}/{{ .serviceName }}/admin$2; + proxy_set_header Accept-Encoding ""; + sub_filter_once off; + sub_filter_types *; + sub_filter 'href="/' 'href="/physical_location/{{ .zenPhysicalLocationName }}/{{ .serviceName }}/'; + sub_filter "href='/" "href='/physical_location/{{ .zenPhysicalLocationName }}/{{ .serviceName }}/"; + sub_filter 'src="/' 'src="/physical_location/{{ .zenPhysicalLocationName }}/{{ .serviceName }}/'; + sub_filter "src='/" "src='/physical_location/{{ .zenPhysicalLocationName }}/{{ .serviceName }}/"; + sub_filter 'action="/' 'action="/physical_location/{{ .zenPhysicalLocationName }}/{{ .serviceName }}/'; + sub_filter "action='/" "action='/physical_location/{{ .zenPhysicalLocationName }}/{{ .serviceName }}/"; + sub_filter 'url("/' 'url("/physical_location/{{ .zenPhysicalLocationName }}/{{ .serviceName }}/'; + sub_filter "url('/" "url('/physical_location/{{ .zenPhysicalLocationName }}/{{ .serviceName }}/"; + sub_filter 'window.location="/' 'window.location="/physical_location/{{ .zenPhysicalLocationName }}/{{ .serviceName }}/'; + sub_filter "window.location='/" "window.location='/physical_location/{{ .zenPhysicalLocationName }}/{{ .serviceName }}/"; + sub_filter 'location.href="/' 'location.href="/physical_location/{{ .zenPhysicalLocationName }}/{{ .serviceName }}/'; + sub_filter "location.href='/" "location.href='/physical_location/{{ .zenPhysicalLocationName }}/{{ .serviceName }}/"; + sub_filter '{window.ROOT_PATH}/' '{window.ROOT_PATH}/physical_location/{{ .zenPhysicalLocationName }}/{{ .serviceName }}/'; + } diff --git a/mcp-gateway-forge/postgresql.template.tgz b/mcp-gateway-forge/postgresql.template.tgz new file mode 100644 index 0000000..8c18900 Binary files /dev/null and b/mcp-gateway-forge/postgresql.template.tgz differ diff --git a/qna_rag_chatbot/Industry-Accelerators/watsonx.ai/LICENSE b/qna_rag_chatbot/Industry-Accelerators/watsonx.ai/LICENSE new file mode 100644 index 0000000..0dd04ed --- /dev/null +++ b/qna_rag_chatbot/Industry-Accelerators/watsonx.ai/LICENSE @@ -0,0 +1,15 @@ +Terms and Conditions +This information contains sample modules, exercises, and code samples (the code may be provided in source code form (“Source Code”)) (collectively “Sample Materials”). + +License: +Subject to the terms herein, you may copy, modify, and distribute these Sample Materials within your enterprise only, for your internal use only; provided such use is within the limits of the license rights of the IBM agreement under which you are licensing any related service. There might be applicable third-party licenses included with the Sample Materials. Review the third-party licenses before you use any of the Sample Materials. You can find the third-party licenses that apply to each Sample Material in the notices.txt file that is included. + +Code Security: +Source Code may not be disclosed to any third parties for any reason without IBM’s prior written consent, and access must be limited to your employees who have a need to know. +You have implemented and will maintain the technical and personnel focused security policies, procedures, and controls that are necessary to protect the Source Code against loss, alteration, unlawful forms of processing, unauthorized disclosure, and unauthorized access. +You will promptly (and in no event any later than 48 hours) notify IBM after becoming aware of any breach or other security incident that you know, or should reasonably suspect, affects or will affect the Source Code or IBM, and will provide IBM with reasonably requested information about such security incident and the status of any remediation and restoration activities. +You will not permit any Source Code to reside on servers located in the Russian Federation, the People’s Republic of China, or any territories worldwide in which the Russian Federation or People’s Republic of China claim sovereignty (collectively, “China or Russia”). Company shall not permit anyone to access or use any such Source Code from or within China or Russia, and Company will not permit any development, testing, or other work to occur in China or Russia that would require such access or use. Upon reasonable written notice, IBM may extend these restrictions to other countries that the United States government identifies as potential cyber security concerns. +IBM may request that you verify compliance with these Code Security obligations, and you agree to cooperate with IBM in that regard. + +General: +Notwithstanding anything to the contrary, IBM PROVIDES THE SAMPLE MATERIALS ON AN "AS IS" BASIS AND IBM DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, ANY IMPLIED WARRANTIES OR CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND ANY WARRANTY OR CONDITION OF NON-INFRINGEMENT. IBM SHALL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR ECONOMIC CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR OPERATION OF THE SAMPLE MATERIALS. IBM SHALL NOT BE LIABLE FOR LOSS OF, OR DAMAGE TO, DATA, OR FOR LOST PROFITS, BUSINESS REVENUE, GOODWILL, OR ANTICIPATED SAVINGS. IBM HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS OR MODIFICATIONS TO THE SAMPLE MATERIALS. diff --git a/qna_rag_chatbot/Industry-Accelerators/watsonx.ai/QnA_chatbot_app/.env b/qna_rag_chatbot/Industry-Accelerators/watsonx.ai/QnA_chatbot_app/.env new file mode 100644 index 0000000..ac5f0ec --- /dev/null +++ b/qna_rag_chatbot/Industry-Accelerators/watsonx.ai/QnA_chatbot_app/.env @@ -0,0 +1,26 @@ +################################################################################## +## BELOW configuration needs to be updated by ADMIN user at time of app deployment +## Q&A RAG accelerator endpoint configuration for connection +################################################################################## +## QNA RAG accelerator external api configuration +QNA_RAG_DEPLOYMENT_URL=https://us-south.ml.cloud.ibm.com/ml/v4/deployments/mmserv/ai_service?version=2021-05-01 +## QNA_RAG_ENV_TYPE can be saas or on-prem +QNA_RAG_ENV_TYPE=saas +################################################################################# +## When QNA_ENV_TYPE is saas, please update below +QNA_RAG_SAAS_IAM_APIKEY= +################################################################################## +## When QNA_ENV_TYPE is on-prem , please update below with base64 encoded user api pair +QNA_RAG_ONPREM_CPD_USERNAME= +QNA_RAG_ONPREM_CPD_APIKEY= +################################################################################## +## QNA Streamlit app initialization settings +################################################################################## +FEEDBACK_RATING_OPTIONS=5 +## Expert Recommendation - true/false +ENABLE_EXPERT_RECOMMENDATION=true +## Is this Sample Expert profile recommendation - true/false +IS_EXPERT_SAMPLE=true + +STREAMLIT_SERVER_ENABLE_XSRF_PROTECTION=false +STREAMLIT_SERVER_ENABLE_CORS=false diff --git a/qna_rag_chatbot/Industry-Accelerators/watsonx.ai/QnA_chatbot_app/Dockerfile b/qna_rag_chatbot/Industry-Accelerators/watsonx.ai/QnA_chatbot_app/Dockerfile new file mode 100644 index 0000000..101e976 --- /dev/null +++ b/qna_rag_chatbot/Industry-Accelerators/watsonx.ai/QnA_chatbot_app/Dockerfile @@ -0,0 +1,11 @@ +FROM registry.access.redhat.com/ubi9/python-311:latest +USER root +RUN mkdir /app +WORKDIR /app +COPY qna_streamlit_chat_app.py /app +COPY requirements.txt /app +COPY .env /app +RUN pip install -r requirements.txt +EXPOSE 8080 + +CMD ["streamlit", "run", "qna_streamlit_chat_app.py", "--server.port", "8080", "--theme.base" , "light", "--client.toolbarMode", "minimal", "--server.enableCORS", "false", "--server.enableXsrfProtection", "false"] diff --git a/qna_rag_chatbot/Industry-Accelerators/watsonx.ai/QnA_chatbot_app/qna_streamlit_chat_app.py b/qna_rag_chatbot/Industry-Accelerators/watsonx.ai/QnA_chatbot_app/qna_streamlit_chat_app.py new file mode 100644 index 0000000..3eef17d --- /dev/null +++ b/qna_rag_chatbot/Industry-Accelerators/watsonx.ai/QnA_chatbot_app/qna_streamlit_chat_app.py @@ -0,0 +1,416 @@ +#Sample Materials, provided under license. +#Licensed Materials - Property of IBM. +#© Copyright IBM Corp. 2024,2025. All Rights Reserved. +#US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp. + +#The Python Imaging Library (PIL) is +# +# Copyright © 1997-2011 by Secret Labs AB +# Copyright © 1995-2011 by Fredrik Lundh and contributors +# +#Pillow is the friendly PIL fork. It is +# +# Copyright © 2010-2024 by Jeffrey A. Clark and contributors + +import streamlit as st +from pydantic import BaseModel +import requests +import time +import uuid +import re +import base64 + +from dotenv import dotenv_values, find_dotenv + +server_config = dotenv_values(find_dotenv()) + +####################### +#theme configuration +dashboard_theme= { + "theme.base": "light", + "theme.backgroundColor": "#F4F4F4", + "theme.primaryColor": "#5591f5", + "theme.secondaryBackgroundColor": "#D1D1D1", + "theme.textColor": "#0a1464" +} + +for theme_key, theme_val in dashboard_theme.items(): + if theme_key.startswith("theme"): st._config.set_option(theme_key, theme_val) + +# read default RAG function credentials from environment +if 'QNA_RAG_DEPLOYMENT_URL' in server_config: + try: + QNA_RAG_DEPLOYMENT_URL = server_config['QNA_RAG_DEPLOYMENT_URL'] + if "/ai_service?" in QNA_RAG_DEPLOYMENT_URL: + print("using QnA RAG 2.x based deployment") + QNA_DEPLOYMENT_VERSION="2.0" + else: + print("using QnA RAG 1.x based deployment") + QNA_DEPLOYMENT_VERSION="1.x" + except: + QNA_RAG_DEPLOYMENT_URL = '' + +if 'QNA_RAG_ENV_TYPE' in server_config: + try: + QNA_RAG_ENV_TYPE = server_config['QNA_RAG_ENV_TYPE'] + except: + QNA_RAG_ENV_TYPE = '' + +if 'QNA_RAG_SAAS_IAM_APIKEY' in server_config: + try: + QNA_RAG_SAAS_IAM_APIKEY = server_config['QNA_RAG_SAAS_IAM_APIKEY'] + except: + QNA_RAG_SAAS_IAM_APIKEY = '' + +if 'QNA_RAG_ONPREM_CPD_USERNAME' in server_config: + try: + QNA_RAG_ONPREM_CPD_USERNAME = server_config['QNA_RAG_ONPREM_CPD_USERNAME'] + except: + QNA_RAG_ONPREM_CPD_USERNAME = '' + +if 'QNA_RAG_ONPREM_CPD_APIKEY' in server_config: + try: + QNA_RAG_ONPREM_CPD_APIKEY = server_config['QNA_RAG_ONPREM_CPD_APIKEY'] + except: + QNA_RAG_ONPREM_CPD_APIKEY = '' + +# set expert recommendation +if 'ENABLE_EXPERT_RECOMMENDATION' in server_config: + try: + EXPERT_RECOMMENDATION= ( True if server_config['ENABLE_EXPERT_RECOMMENDATION'].lower() == "true" else False) + except: + EXPERT_RECOMMENDATION = False + +# set sample expert recommendation +sample_recommendation="" +if 'IS_EXPERT_SAMPLE' in server_config: + try: + sample_recommendation= ( "This is synthetic expert profile generated using watsonx.ai." if server_config['IS_EXPERT_SAMPLE'].lower() == "true" else "") + except: + sample_recommendation = '' + +feedback_expert_text="" +if EXPERT_RECOMMENDATION: + feedback_expert_text="If you want to know better answer for this question, Please click on above expert recommendation! \n Otherwise post your next question. " + + +# model for single chat message +class msg_entry(BaseModel): + id: str + role: str = 'assistant' + text: str = 'write' + documents: list[dict] = [] + show_documents: bool = False + log_id: str = '' + rating_options: int = 5 + +# retrieve and cache IAM access token +AccessToken = '' +AccessTokenExpires = 0 +def get_token(force=False): + global AccessToken, AccessTokenExpires + now = time.time() + if AccessTokenExpires <= now or force: + + if QNA_RAG_ENV_TYPE == "saas": + if not QNA_RAG_SAAS_IAM_APIKEY or not QNA_RAG_DEPLOYMENT_URL: + st.error("Please provide RAG function credentials for watsonx.ai aas") + return '' + # get access token for watsonx.ai SaaS + response = requests.post('https://iam.cloud.ibm.com/identity/token', data={'apikey': QNA_RAG_SAAS_IAM_APIKEY, 'grant_type': 'urn:ibm:params:oauth:grant-type:apikey'}) + if response.status_code == 200: + resp = response.json() + if not 'access_token' in resp: + st.error("Unexpected token format.") + return '' + + AccessToken = resp['access_token'] + # calculate expiration time (as a precaution, subtract 5 minutes) + AccessTokenExpires = ( now + resp['expires_in'] if 'expires_in' in resp else ( resp['expiration'] if 'expiration' in resp else now ) ) - 300 + else: + st.error("QNA_RAG_ENV_TYPE as on-prem or saas only supported") + return '' + + return AccessToken + + +# call RAG function +def exec_request(payload, qna_url, ignore_errors=False ): + header={} + if QNA_RAG_ENV_TYPE == "on-prem": + if not QNA_RAG_ONPREM_CPD_USERNAME or not QNA_RAG_ONPREM_CPD_APIKEY or not QNA_RAG_DEPLOYMENT_URL: + st.error("Please provide all credentials required for watsonx.ai on-prem") + return '' + # get access token for watsonx.ai On Prem + user_pass_string=QNA_RAG_ONPREM_CPD_USERNAME+ ':' + QNA_RAG_ONPREM_CPD_APIKEY + AccessToken=base64.b64encode(user_pass_string.encode()).decode() + header={'Content-Type': 'application/json', 'Authorization': 'ZenApiKey '+AccessToken} + else: + header = {'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Bearer '+get_token()} + status_code = 0 + try: + response = requests.post(qna_url, json=payload, headers=header, verify=False) + status_code = response.status_code + if not status_code == 200 and not ignore_errors: + st.error(f"Error with status code: {status_code}") + except: + st.error(f"Connecting RAG function failed.") + response = None + + return response + + +# run RAG function to generate response +def get_response(prompt): + qna_url=QNA_RAG_DEPLOYMENT_URL + if QNA_DEPLOYMENT_VERSION=="1.x": + + response_scoring = exec_request({"input_data": [{"fields": ["Text"], "values": [[prompt]]}]}, qna_url) + # extract response, source documents and log id + if response_scoring == None: + return 'I am not able to reply due to a technical issue..', [], '' + + response = response_scoring.json() + text = response['predictions'][0]['values'][0][0]['response'] + documents = response['predictions'][0]['values'][0][0].get('source_documents', []) + log_id = response['predictions'][0]['values'][0][0].get('log_id', '') + else: + qna_url = QNA_RAG_DEPLOYMENT_URL.replace("/ai_service?", "/ai_service/qna?") + response_scoring = exec_request({"question": prompt}, qna_url) + # extract response, source documents and log id + if response_scoring == None: + return 'I am not able to reply due to a technical issue..', [], '' + + response = response_scoring.json() + text = response['result']['response'] + documents = response['result'].get('source_documents', []) + log_id = response['result'].get('log_id', '') + + return text, documents, log_id + + +# run RAG function to update feedback +def send_feedback(log_id, value, comment=None): + st.session_state.active = '' + + if not log_id == '': + feedback_Value=int(value) + if feedback_Value < 100 and comment == None: + st.session_state.feedback = value + st.session_state.log_id = log_id + new_msg = msg_entry(id=str(uuid.uuid4()), role='assistant', text='Thanks for your feedback! Please add a comment in below input box.') + st.session_state.history.append(new_msg) + return None + else: + if QNA_DEPLOYMENT_VERSION=="1.x": + qna_url=QNA_RAG_DEPLOYMENT_URL + response_update = exec_request({"input_data": [{"fields": ["log_id", "value", "comment"], "values": [[log_id, value, comment if not comment == None else '']]}]}, qna_url) + if feedback_Value < 100: + new_msg = msg_entry(id=str(uuid.uuid4()), role='assistant', \ + text='Thanks! Feedback has been sent. \n'+ feedback_expert_text if response_update.status_code == 200 and response_update.json()['predictions'][0]['values'][0][0] == 'ok' else 'Feedback update got failed' + feedback_expert_text) + st.session_state.expert_disabled=False + if EXPERT_RECOMMENDATION: + st.button('Recommended Expert for this question', 'expert_toggle'+new_msg.id, on_click=get_expert_recommendation, args=(log_id,),disabled=st.session_state.expert_disabled) + else: + new_msg = msg_entry(id=str(uuid.uuid4()), role='assistant', \ + text='Thanks! Feedback has been sent. \n Type your next question in input box.' if response_update.status_code == 200 and response_update.json()['predictions'][0]['values'][0][0] == 'ok' else 'Feedback update got failed. Type your next Question in input box') + else: + qna_url = QNA_RAG_DEPLOYMENT_URL.replace("/ai_service?", "/ai_service/log_feedback?") + response_update = exec_request({"log_id":log_id, "value":value, "comment":comment if not comment == None else ''}, qna_url) + if feedback_Value < 100: + new_msg = msg_entry(id=str(uuid.uuid4()), role='assistant', \ + text='Thanks! Feedback has been sent. \n'+ feedback_expert_text if response_update.status_code == 200 and response_update.json()['status'] == 'ok' else 'Feedback update got failed' + feedback_expert_text) + st.session_state.expert_disabled=False + if EXPERT_RECOMMENDATION: + st.button('Recommended Expert for this question', 'expert_toggle'+new_msg.id, on_click=get_expert_recommendation, args=(log_id,),disabled=st.session_state.expert_disabled) + else: + new_msg = msg_entry(id=str(uuid.uuid4()), role='assistant', \ + text='Thanks! Feedback has been sent. \n Type your next question in input box.' if response_update.status_code == 200 and response_update.json()['status'] == 'ok' else 'Feedback update got failed. Type your next Question in input box') + if comment == None: + st.session_state.history.append(new_msg) + return None + else: + return new_msg + +# run RAG function to get expert recommendation +def get_expert_recommendation(log_id): + st.session_state.active = '' + st.session_state.expert_recommendation = '' + if not log_id == '': + if QNA_DEPLOYMENT_VERSION=="1.x": + qna_url=QNA_RAG_DEPLOYMENT_URL + expert_response_update = exec_request({"input_data": [{"fields": ["_function", "log_id"], "values": [["recommend_top_experts", log_id]] }]}, qna_url) + expert_msg = msg_entry(id=str(uuid.uuid4()), role='assistant', \ + text=str(sample_recommendation) + " Please contact below expert for more details." + + "\n \n **Name**: "+ expert_response_update.json()['predictions'][0]['values'][0][0][0]["name"] + + ",\n \n **Email**: " + expert_response_update.json()['predictions'][0]['values'][0][0][0]["email"] + + ",\n \n **Designation**: " + expert_response_update.json()['predictions'][0]['values'][0][0][0]["position"] + + ",\n \n **Industry**: " + expert_response_update.json()['predictions'][0]['values'][0][0][0]["domain"] + + ".\n \n Please type your next question in below input box!" if expert_response_update.status_code == 200 and 'expert_details' in expert_response_update.json()['predictions'][0]['values'][0][1] else "No Experts found on this topic, Please ask next Question!") + + else: + qna_url = QNA_RAG_DEPLOYMENT_URL.replace("/ai_service?", "/ai_service/recommended_experts?") + expert_response_update = exec_request({"log_id": log_id}, qna_url) + expert_msg = msg_entry(id=str(uuid.uuid4()), role='assistant', \ + text=str(sample_recommendation) + " Please contact below expert for more details." + + "\n \n **Name**: "+ expert_response_update.json()['recommended_experts'][0]["name"] + + ",\n \n **Email**: " + expert_response_update.json()['recommended_experts'][0]["email"] + + ",\n \n **Designation**: " + expert_response_update.json()['recommended_experts'][0]["position"] + + ",\n \n **Industry**: " + expert_response_update.json()['recommended_experts'][0]["domain"] + + ".\n \n Please type your next question in below input box!" if expert_response_update.status_code == 200 and 'expert_details' in expert_response_update.json()['expert_status'] else "No Experts found on this topic, Please ask next Question!") + + # text='Please contact expert '+response_update.json()['predictions'][0]['values'][0][0][0]["name"] + ' , Email id: '+ response_update.json()['predictions'][0]['values'][0][0][0]["email"] + 'Profile Info: '+ response_update.json()['predictions'][0]['values'][0][0][0]["text"] if response_update.status_code == 200 and response_update.json()['predictions'][0]['values'][0][1] == 'expert_details retrieved from log records' else 'No Experts found on this topic') + st.session_state.expert_disabled=True + st.session_state.history.append(expert_msg) + + return None + +# ping RAG function +def ping(): + qna_url=QNA_RAG_DEPLOYMENT_URL + if QNA_DEPLOYMENT_VERSION=="1.x": + default_payload={"input_data": [{"fields": [""], "values": [[""]] }]} + else: + default_payload={"":""} + response_ping = exec_request(default_payload, qna_url, ignore_errors=True) + status_code = response_ping.status_code if not response_ping == None else 0 + if not status_code == 200: + return False, status_code + response = response_ping.json() + # expected response: {'predictions': [{'fields': ['status'], 'values': [['invalid parameters']]}]} + if QNA_DEPLOYMENT_VERSION=="1.x": + response_ok='predictions' in response and len(response['predictions']) > 0 and len(response['predictions'][0]['fields']) > 0 and response['predictions'][0]['fields'][0] == 'status' + else: + response_ok='status' in response and len(response['status']) > 0 + return response_ok, status_code + + +def get_msg_by_id(id): + if not 'history' in st.session_state: + return None + hits = [index for index in range(len(st.session_state.history)) if st.session_state.history[index].id == id] + return st.session_state.history[hits[0]] if len(hits) > 0 else None + +# toggle displaying documents in chat message +def show_hide_documents(id): + hits = [index for index in range(len(st.session_state.history)) if st.session_state.history[index].id == id] + if len(hits) > 0: + st.session_state.history[hits[0]].show_documents = not st.session_state.history[hits[0]].show_documents + + +# print single chat message +def print_message(msg: msg_entry, save=False): + with st.chat_message(msg.role): + st.markdown(msg.text) + if msg.show_documents: + m = f'' + if len(msg.documents) > 0: + m = f'{m}' + + for d in msg.documents: + m = f'{m}' + + m = f'{m}
Title/SourceDocument
{d["metadata"]["title"]}{d["page_content"]}



' + else: + m = f'{m}No documents were used.' + st.markdown(m,unsafe_allow_html=True) + if not msg.log_id == '': + disabled = not msg.id == st.session_state.active + st.session_state.expert_disabled = True + cols = st.columns([1 for _ in range(msg.rating_options)] + [9 - msg.rating_options]) + # 0 1 2 3 4 5-ok 6 7 8 + options_list = ['👎', '👍', '😡', '😠', '🙁', '😐', '🙂', '😀', '🤩'] + options_selector = [[0,1],[3,5,7],[2,3,7,8],[2,3,5,7,8]] + for _i in range(msg.rating_options-1, -1, -1): + cols[msg.rating_options-_i-1].button(options_list[options_selector[msg.rating_options-2][_i]], f"opt{str(_i)}{msg.id}", \ + on_click=send_feedback, args=(msg.log_id,str(round(100*(_i/(msg.rating_options-1)))),), disabled=disabled) + st.button(('Hide' if msg.show_documents else 'Show') + ' source documents', 'toggle'+msg.id, on_click=show_hide_documents, args=(msg.id,)) + if save: + st.session_state.history.append(msg) + + + +########################################################################################## +## Refresh page + +# feedback value to be sent after comment imput +if not 'feedback' in st.session_state: + st.session_state.feedback = '' + +# current log id +if not 'log_id' in st.session_state: + st.session_state.log_id = '' + +# msg id with active feedback buttons +if not 'active' in st.session_state: + st.session_state.active = '' + +# default feedback rating options +if not 'rating_options' in st.session_state: + st.session_state.rating_options = int(server_config["FEEDBACK_RATING_OPTIONS"]) + +st.set_page_config(page_title='QnA with RAG Bot', layout = "wide") +st.title("Q&A with RAG interactive streamlit based UI app") +st.session_state.connection=False +#st.QNA_DEPLOYMENT_VERSION=QNA_DEPLOYMENT_VERSION + +ok, status_code = ping() +if ok: + st.session_state.connection=True +else: + st.error(f"Connection test failed, status code: {str(status_code)}") + st.session_state.connection=False + +_sub_title_description , _reset_button = st.columns([0.85,0.15], gap="large") + +if not st.session_state.connection: + _sub_title_description.error("This dashboard cannot be generated! Please configure your app with deployment function properly! Contact Adminstrator") + st.stop() +else: + match = re.search(r'/deployments/([^/]*)/', QNA_RAG_DEPLOYMENT_URL) + _sub_title_description.markdown("This is QnA chatbot - Retrieval Augmented Generation by calling deployment function "+ f"**{match.group(1)}**" + " of watsonx.ai " + QNA_RAG_ENV_TYPE ) + _sub_title_description.markdown(f"\nTo clear chat history, click on reset chat button on the right side." ) + +if _reset_button.button('Reset chat'): + if 'history' in st.session_state: + del st.session_state.history + st.session_state.feedback = '' + st.session_state.active = '' + st.session_state.log_id = '' + st.session_state.expert_disabled = True + +# chat history (with welcome message) +if not 'history' in st.session_state: + st.session_state.history = [msg_entry(id=str(uuid.uuid4()), role='assistant', \ + text="Hi there! I am a QnA chatbot, please type your question in below input box!")] + +# user's input +if prompt := st.chat_input("Ask questions and share feedback comment in this same input box."): + + # print chat (all feedback buttons inactive) + st.session_state.active = '' + for _msg in st.session_state.history: + print_message(_msg) + + new_msg = msg_entry(id=str(uuid.uuid4()), role='user', text=prompt) + print_message(new_msg, save=True) + + if not st.session_state.feedback == '': + # update feedback + new_msg = send_feedback(st.session_state.log_id, st.session_state.feedback, prompt) + st.session_state.feedback = '' + #elif not st.session_state.recommendation == '': + + else: + # run RAG function + with st.spinner("Please wait, QnA chatbot is typing.."): + text, documents, log_id = get_response(prompt) + new_msg = msg_entry(id=str(uuid.uuid4()), role='assistant', text=text, documents=documents, log_id=log_id, rating_options=st.session_state.rating_options ) + + st.session_state.active = new_msg.id if not new_msg.log_id == '' else '' + print_message(new_msg, save=True) + +else: + # print chat (with active feedback button) + for _msg in st.session_state.history: + print_message(_msg) diff --git a/qna_rag_chatbot/Industry-Accelerators/watsonx.ai/QnA_chatbot_app/readme.md b/qna_rag_chatbot/Industry-Accelerators/watsonx.ai/QnA_chatbot_app/readme.md new file mode 100644 index 0000000..9662f27 --- /dev/null +++ b/qna_rag_chatbot/Industry-Accelerators/watsonx.ai/QnA_chatbot_app/readme.md @@ -0,0 +1,225 @@ +# Sample Streamlit app for QnA with RAG + +This is Sample Streamlit app provides a UI interface for the `Q&A with RAG accelerator`. It allows a user to ask questions about a given corpus of documents which are answered using a Retrieval Augmented Generation (RAG) approach. Along with the answer, the app references the source documents used to generate the answer. In addition, the user can provide feedback in a configurable range along with a free text comment. There is also an option to recommend a subject matter expert if the user is not satisfied with the answer. + +This app requires an external API which is provided by the `Q&A with RAG accelerator` project template deployed on `Watsonx.ai` `SaaS` or `On Prem`. +For the IBM Cloud SaaS version, please refer to the [IBM Resource Hub](https://dataplatform.cloud.ibm.com/exchange/public/entry/view/75b22cbe-8a20-44a5-ac65-3a927e92cb0e?context=wx). For the CPD On Prem version, access to the `Q&A with RAG accelerator` can be requested through your IBM client team. The `Q&A with RAG accelerator` must be configured and deployed according to the needs of your use case. + +## Features +- UI interface that sends user queries to the API and displays the response based on Q&A RAG deployment on watsonx.ai. +- Feedback rating options to select. +- Toggle button to display or hide the source documents. +- Feedback buttons (based on rating options selected) for each response. By default - we have added support from min 2 to 5 max feedback ratings in this app. +- Expert Recommendations are available for each user question based on user rating below 100%. (Optional) + +## Requirements +- `Python3` for local setup both py3.9 and py3.11 versions works. +- `Docker` or `Podman` container engine to build your local container image.[Optional] +- `Q&A RAG Accelerator` project setup on `Watsonx.ai` `aaS` or `On Prem` must be deployed. + +## Pre - Reqs to run this app +- Please make sure to run Q&A RAG pipeline and copy deployment function url from the deployment space where it is deployed on Watsonx.ai aaS or On Prem environments. +- Admin user should update the .env file in this folder. Must gather details of watsonx.ai endpoint configuration details before starting this. +- Clone or download this repository. +- Navigate to the project directory `QnA_chatbot_app` +- Please make sure to update the `.env` hidden file according to your specific use case. + + - To configure the endpoint of the Q&A RAG Accelerator for connection with the Streamlit app: [Required] + + - `QNA_RAG_DEPLOYMENT_URL` should be set to the public endpoint URL of the QnA RAG function, either on watsonx.ai SaaS or on-prem. + - To obtain this URL, navigate to the `Deployments` tab. Select the deployment space used in your watsonx.ai project, then click on the Deployments tab to view the online deployments. Look for one named `rag_scoring_function_with_elasticsearch` or `rag_scoring_function_with_milvus`. If these are not available, please run the Q&A RAG pipeline to deploy it quickly. + - Once deployed, check the deployment details and choose the one with the matching `serving name` based on the `deployment serving name` parameter provided in your watsonx.ai **Q&A with RAG Accelerator** project. Then select the deployment under `API reference` & copy the `Public endpoint` url. For eg: + - For 1.x RAG deployments + - For SaaS deployments see [link here](https://dataplatform.cloud.ibm.com/ml-runtime/deployments), and the endpoint URL will be in the following format: + ``` + https://.ml.cloud.ibm.com/ml/v4/deployments//predictions?version=2021-05-01 + ``` + - For on-prem deployments, check `https:///ml-runtime/deployments` link by updating your cluster details and the endpoint URL will be: + ``` + https:///ml/v4/deployments//predictions?version=2021-05-01 + ``` + - For 2.x RAG deployments + - For SaaS deployments see [link here](https://dataplatform.cloud.ibm.com/ml-runtime/deployments), and the endpoint URL will be in the following format: + ``` + https://.ml.cloud.ibm.com/ml/v4/deployments//ai_service?version=2021-05-01 + ``` + - For on-prem deployments, check `https:///ml-runtime/deployments` link by updating your cluster details and the endpoint URL will be: + ``` + https:///ml/v4/deployments//ai_service?version=2021-05-01 + ``` + - Please update your Watsonx-based endpoints accordingly. + - Please Update `QNA_RAG_ENV_TYPE`, an environment type where your deployed Q&A with RAG Accelerator. Supported values `saas` likely on IBMCloud or `on-prem` based CPD SW clusters. [Required] + - When `QNA_RAG_ENV_TYPE` to `saas`, please update below + - `QNA_RAG_SAAS_IAM_APIKEY` is IAM key of watsonx.ai project which has access to the Q&A RAG Accelerator project and its deployment space which you already used in your wx.ai aaS project + - When `QNA_RAG_ENV_TYPE` to `on-prem`, please update below + - `QNA_RAG_ONPREM_CPD_USERNAME` is username of your cpd based on prem cluster + - `QNA_RAG_ONPREM_CPD_APIKEY` is API key generated to access your on prem based cpd cluster. + - To initialize the streamlit app for QnA [Optional]. Default values are already set. + - `FEEDBACK_RATING_OPTIONS` is set to `5` by default, admin user can configure this. app can support feedback rating options from 2 to 7. + - `ENABLE_EXPERT_RECOMMENDATION` is set to `false` , enable this if you need to enable expert recommendations by default + - `IS_EXPERT_SAMPLE` is set to `true`, disable if you have ingested your own expert profiles instead of sample + +## How to Run locally + +### Pre Reqs +- To run locally make sure you already installed python3 based environment. + +### Steps +1. Create a virtual environment and activate it (optional): + ```bash + python3.11 -m venv venv + source venv/bin/activate + ``` +2. Install dependencies: + ```bash + pip install -r requirements.txt + ``` +3. Run the app: + ```bash + streamlit run qna_streamlit_chat_app.py --server.port 8080 --client.toolbarMode minimal --theme.base light + ``` + Note: update port number which is not in use already. +4. Open the app in your browser using the provided URL from above output in your terminal + + +## How To Run and deploy on IBM Cloud code Engine + +- This procedure uses IBM Code Engine CLI to deploy the application. + **NOTE** Please check IBM Code Engine official documentation if you plan to deploy via UI or looking for other deployment options https://cloud.ibm.com/docs/codeengine?topic=codeengine-application-workloads +- please make sure to install `ibmcloud`.Official doc link is https://cloud.ibm.com/docs/cli/reference/ibmcloud?topic=cloud-cli-getting-started +- Make sure you have access to IBM Code engine as well if you are planning to deploy this app there. +- Setup IBM Code engine CLI - https://cloud.ibm.com/docs/codeengine?topic=codeengine-install-cli + +### Deploy on IBM Cloud CE from your local source code. + +- Admin user can use IBM Cloud Repository and IBM Cloud code engine to deploy their app which is by following below steps from their local source code. +- In this procedure, private IBM cloud repository will be used by Code Engine to build your image and use for deployment automatically via background process in a secure way & this has less pre-reqs to deploy your app. + +#### Steps to deploy on Cloud +- Make sure you have required details below to deploy + +1. Logon to IBM Cloud via apikey: + ``` + ibmcloud login --apikey -r + ``` + **Note** for each IBMaccount you have different api key. please make sure to use right account and its apikey which has enough permissions to access IBM Code Engine and IBM Container Repository to deploy this app. +2. Install IBM code engine plugin if not done already + `ibmcloud plugin install code-engine` +3. Make sure to setup target region, account, resourcegroup on your ibmcloud client. Make sure you select resource group which has enough permissions to access IBM Code Engine and IBM Container Repository to deploy this app. + `ibmcloud target -g ` +4. Create project on your code engine account if you don't have existing one - https://cloud.ibm.com/docs/codeengine?topic=codeengine-manage-project + `ibmcloud ce project create -n ` + **Note** Make sure you have enough quota to create a project with your IBM cloud account. +6. Select the project where you want to deploy + `ibmcloud ce project select -n ` +7. Make sure you are in `QnA_chatbot_app` folder location. +8. Then Create and Deploy your streamlit application on Code Engine & wait for it to complete. + - `ibmcloud ce app create --name --build-source .`
**Note** You might face issues with quota limits or IAM configurations with your IBM cloud account. Please check troubleshoot steps before running above cmd. +
The . indicates the build source is located in your current working directory or specify the path where `QnA_chatbot_app` folder exists. +9. Get your public deployment on Code Engine by listing your app name when it is Ready. + `ibmcloud ce app list | grep ` +10. Copy the URL and paste on your browser to connect your streamlit app. it may take few secs to bring the app up. +11. To check logs of the application, please run below. + `ibmcloud ce app logs --name ` + + +### How to deploy on Prem RedHat OCP + +This procedure requries On Prem Cluster based on RedHat Openshift, OpenShift CLI and Container Engine(Optional) + +#### Pre-reqs +- Install OC cli follow steps from official documentation https://docs.redhat.com/en/documentation/openshift_container_platform/4.13/html/cli_tools/openshift-cli-oc#cli-installing-cli_cli-developer-commands +- Install Container Registry like Podman/Docker & also make use existing Openshift Container Engine is exposed on Cluster to use. +- RH Cluster details and its Kubeadmin level creds to access. +- Any Linux node with minimum configuration or infra node of cluster + +#### Steps to deploy on Prem (Required advanced admin previlages) + +1. Log on to the Red Hat OpenShift Container Platform cluster using the OpenShift Cli from any linux node or infra node. + - Please provide required creds for login to the OC cluster and run below cmd. Need kubeadmin privilages. + `oc login --insecure-skip-tls-verify=true -u -p ` + +2. create in-cluster image registry default route + ``` + oc patch configs.imageregistry.operator.openshift.io/cluster --type merge -p '{"spec"{"defaultRoute":true}} -n openshift-image-registry' + ``` + - once enabled. you should see details default route for container engine exposed. + for example below, Host/port: + ``` + oc get routes -n openshift-image-registry + NAME HOST/PORT PATH SERVICES PORT TERMINATION WILDCARD + default-route default-route-openshift-image-registry.apps..com image-registry reencrypt None + ``` + - check if default route is exposed by running below cmd which should same as route host as above for example. + `oc registry info -n openshift-image-registry` +3. Login to container registry using the installed local container engine + ` login -u -p $(oc whoami -t) $(oc registry info -n openshift-image-registry) --tls-verify=false` +4. Navigate to location of QnA_chatbot_app where you downloaded this repo. Assuming you have already updated required env updates in this project. +5. Build a Contianer image of the application that needs to be deployed to an OpenShift cluster + ` build -t qna_streamlit_chat_app:v1.6 .` +6. Create a new namespace for the application + `oc new-project ` +7. Tag the container image as shown below - make sure to update the registry url accordingly. + ` tag qna_streamlit_chat_app:v1.6 $(oc registry info -n openshift-image-registry)//qna_streamlit_chat_app:v1.6` +8. Push the Container image to the OpenShift image registry. + ` push $(oc registry info -n openshift-image-registry)//qna_streamlit_chat_app:v1.6 --tls-verify=false` +9. Verify if above image is pushed. + `oc get is -n ` +10. Deploy the image to OpenShift and expose the Route: + `oc new-app --image-stream="/qna_streamlit_chat_app:v1.6" --name= -n ` + `oc expose svc/ -n ` +11. Retreive the deployed application service details for and get the host url + `oc get routes -n ` + +##### Note +- This on prem support deployment is on OCP which is outside the CPD. End user needs to manage their deployments in this case and reachout to RH OCP documenation where ever necessary if you are failing any failures. +- To support on CPD, we are working on this to enhance this deployment to run inside CPD once solution is supported in future release. + +## Troubleshoot + +- While running step 8 to deploy app with cmd `ibmcloud ce app create --name --build-source .` you may face issues like below. + - Make sure your check your IAM account if you have enabled Restrict service ID creation if yes then you will see below failure
`The permission to create a service ID, which has access to IBM Container Registry, is insufficient`
+ - To fix this, you have disable it by running below steps.
+ On IBM Cloud Console, go to `Manage > Access (IAM)`, and select `Settings`.
In the `Account` section, **Disable** `Restrict service ID creation` and confirm.
**Note** Only User with Account owner/admin has required permissions to update this. + - Please make sure that you have enough resource/quota limits on container registry or code engine application to run. If not please increase your quota limits or delete unused older applications/images on ibmcloud. + - check existing applications with `ibmcloud ce app list` identify any unused apps. if have limited quota limit, either delete by running below cmd or increase your quota limits for application + `ibmcloud ce app delete --name ` + - check IBM Container Repository images which were previously created. if have limited quota limit, either delete unused images or increase your quota limits for images storage. + - [Optional] if you havn't installed plugin for IBM Continer Registry. Please run below + `ibmcloud plugin install container-registry` + - To delete any outdated previous images. fetch image details using `ibmcloud cr images` and then retrieve corresponding Repository and tag name of the image created for your app. + `ibmcloud cr image-rm :` + - During This step IBM Code Engine tries build the application based on your local source code. Make sure you don't accidentally create/add any new files/folders under `QnA_chatbot_app` local folder. In case you have added any thing new which is not required for your application. please add them to `.ceignore` file before running the cmd. + +## Terms of use +**Sample Materials, provided under license.
** +Licensed Materials - Property of IBM.
+© Copyright IBM Corp. 2024,2025. All Rights Reserved.
+US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
+ +**The pillow library is used in this application. Pillow is licensed under the open source MIT-CMU License:
+The Python Imaging Library (PIL) is
** + Copyright © 1997-2011 by Secret Labs AB
+ Copyright © 1995-2011 by Fredrik Lundh and contributors
+Pillow is the friendly PIL fork. It is
+ Copyright © 2010-2024 by Jeffrey A. Clark and contributors
+Like PIL, Pillow is licensed under the open source MIT-CMU License:
+By obtaining, using, and/or copying this software and/or its associated
+documentation, you agree that you have read, understood, and will comply
+with the following terms and conditions:
+
+Permission to use, copy, modify and distribute this software and its
+documentation for any purpose and without fee is hereby granted,
+provided that the above copyright notice appears in all copies, and that
+both that copyright notice and this permission notice appear in supporting
+documentation, and that the name of Secret Labs AB or the author not be
+used in advertising or publicity pertaining to distribution of the software
+without specific, written prior permission.
+
+SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
+SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
+IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL,
+INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/qna_rag_chatbot/Industry-Accelerators/watsonx.ai/QnA_chatbot_app/requirements.txt b/qna_rag_chatbot/Industry-Accelerators/watsonx.ai/QnA_chatbot_app/requirements.txt new file mode 100644 index 0000000..6c1d854 --- /dev/null +++ b/qna_rag_chatbot/Industry-Accelerators/watsonx.ai/QnA_chatbot_app/requirements.txt @@ -0,0 +1,6 @@ +pydantic==2.11.7 +pydantic_core==2.33.2 +requests==2.32.4 +streamlit==1.46.0 +urllib3==2.5.0 +python-dotenv==1.1.1 diff --git a/qna_rag_chatbot/README.md b/qna_rag_chatbot/README.md new file mode 100644 index 0000000..fc0f801 --- /dev/null +++ b/qna_rag_chatbot/README.md @@ -0,0 +1,23 @@ +# Deploy qna chatbot application to software hub dataplane + +[Q&A with RAG accelerator](Industry-Accelerators/watsonx.ai/QnA_chatbot_app/readme.md) is Sample Streamlit app that allows a user to ask questions about a given corpus of documents which are answered using a Retrieval Augmented Generation (RAG) approach, app is cloned from [here](https://github.com/IBM/Industry-Accelerators/tree/master/watsonx.ai/QnA_chatbot_app) + +this is the instruction doc for deploying the app into software hub default dataplane: +- follow the [application readme](Industry-Accelerators/watsonx.ai/QnA_chatbot_app/readme.md) to check and setup pre-requiste for deploying the application, this would require deployment of watsonx service endpoints into ibm cloud watsonx service, which will be created when you deploy [IBM Resource Hub](https://dataplatform.cloud.ibm.com/exchange/public/entry/view/75b22cbe-8a20-44a5-ac65-3a927e92cb0e?context=wx), this application will be interacting with the service endpoints through UI interface for Q&A sessions +- checkout the [pre-requisites](../README.md#pre-requisites-to-deploy-sample-applications) required before deploying this application into default dataplane + +#### Steps to deploy +1. Update `QNA_RAG_DEPLOYMENT_URL` and `QNA_RAG_SAAS_IAM_APIKEY` from your deployment environment in qna_rag_chatbot/Industry-Accelerators/watsonx.ai/QnA_chatbot_app/.env +2. Run the follwing create-dockerfile-application command +``` +./cpd-cli manage create-dockerfile-application --instance_ns= --app_name=qna-rag-chatbot --app_port=8080 --app_port_tls=false --repo_url=https://github.com/IBM/swhub-custom-app-samples.git --repo_token= --repo_branch= --repo_app_dir=qna_rag_chatbot/Industry-Accelerators/watsonx.ai/QnA_chatbot_app --cpu=400m --memory=200Mi --cpu_limit=500m --memory_limit=400Mi +``` +3. Ensure the pods are running in the workload namespace +``` +qna-rag-chatbot-l66n2b4a5tra-1-build 0/1 Completed 0 5d +qna-rag-chatbot-l66n2b4a5tra-7b75f9d697-s8hqb 1/1 Running 0 5d +qna-rag-chatbot-jdsgxjtix71p-6bf7b7c4f7-mz78g 1/1 Running 0 8d +qna-rag-chatbot-three-vjzbpxir2vqo-79869dd7cf-m67xd 1/1 Running 0 8d +``` + +#### application available at `https://zen-route/physical_location/default-pl//` diff --git a/ruby-hello-world/README.md b/ruby-hello-world/README.md new file mode 100644 index 0000000..e289134 --- /dev/null +++ b/ruby-hello-world/README.md @@ -0,0 +1,40 @@ +# Deploy ruby-hello-world application to software hub default dataplane + +ruby-hello-world is a web UI application that demostrates openshift template, the application can be deployed into software hub default dataplane as an template application + +Checkout the [pre-requisites](../README.md#pre-requisites-to-deploy-sample-applications) required before deploying this application. + +### Steps to deploy ruby-hello-world + +1. Run this command to change the directory to work directory inside cpd-cli binary `cd cpd-cli-workspace/olm-utils-workspace/work/` +2. Create a file `.yaml` +3. Copy the contents of `application-template-stibuil.yaml` and paste in the file created in step 2. +4. RUN `tar -cvzf .tar.gz .yaml` +5. change the directory back to cpd-cli: `cd ../../..` +6. Run `./cpd-cli manage create-oc-template-application --help` to know more about other options available. +7. Run the create template application command: + +``` +./cpd-cli manage create-oc-template-application --instance_ns=zen --app_name=ruby-hello-world --app_tar_file=/tmp/work/.tar.gz from step 4> --cpu=400m --memory=200Mi --cpu_limit=500m --memory_limit=400Mi +``` + +8. Check the pods in workload namespace + +``` +oc get po +NAME READY STATUS RESTARTS AGE +database-75bff5c968-bjkx8 1/1 Running 0 31m +frontend-c5c765c45-g99fm 1/1 Running 0 30m +frontend-c5c765c45-gtbx2 1/1 Running 0 30m +``` + +9. Update the app proxy config - copy `ruby-hello-world.conf.yaml` to `cpd-cli-workspace/olm-utils-workspace/work/` +``` +./cpd-cli manage update-custom-application-proxy-config \ +--instance_ns=zen \ +--app_name=ruby-hello-world \ +--app_run_id= \ +--app_proxy_config_yaml=/tmp/work/ruby-hello-world.conf.yaml +``` + +10. Check the application running at: `https:///physical_location//-/frontend/` diff --git a/ruby-hello-world/application-template-stibuil.yaml b/ruby-hello-world/application-template-stibuil.yaml new file mode 100644 index 0000000..9b400ad --- /dev/null +++ b/ruby-hello-world/application-template-stibuil.yaml @@ -0,0 +1,262 @@ +kind: Template +apiVersion: template.openshift.io/v1 +metadata: + name: ruby-helloworld-sample + annotations: + description: This example shows how to create a simple ruby application in openshift origin v3 + iconClass: icon-ruby + tags: instant-app,ruby,mysql + labels: + icpdsupport/organizationId: "{{ .zenOrganizationId }}" + icpdsupport/dataPlaneId: "{{ .zenDataplaneId }}" + icpdsupport/dataPlaneName: "{{ .zenDataplaneName }}" + icpdsupport/physicalLocationId: "{{ .zenPhysicalLocationId }}" + icpdsupport/physicalLocationName: "{{ .zenPhysicalLocationName }}" + zenCloudPakInstanceId: "{{ .zenCloudPakInstanceId }}" + zenOrganizationName: "{{ .zenOrganizationName }}" + icpdata_run_id: '{{.RUN_ID}}' +objects: +- kind: Secret + apiVersion: v1 + metadata: + name: dbsecret + stringData: + mysql-user: ${MYSQL_USER} + mysql-password: ${MYSQL_PASSWORD} +- kind: Service + apiVersion: v1 + metadata: + name: frontend + spec: + ports: + - name: web + protocol: TCP + port: 5432 + targetPort: 8080 + nodePort: 0 + selector: + name: frontend + type: ClusterIP + sessionAffinity: None + status: + loadBalancer: {} +- kind: Route + apiVersion: route.openshift.io/v1 + metadata: + name: route-edge + annotations: + template.openshift.io/expose-uri: http://{.spec.host}{.spec.path} + spec: + host: www.example.com + to: + kind: Service + name: frontend + tls: + termination: edge + status: {} +- kind: ImageStream + apiVersion: image.openshift.io/v1 + metadata: + name: origin-ruby-sample + spec: {} + status: + dockerImageRepository: "" +- kind: ImageStream + apiVersion: image.openshift.io/v1 + metadata: + name: ruby-27 + spec: + dockerImageRepository: registry.access.redhat.com/ubi8/ruby-27 + status: + dockerImageRepository: "" +- kind: BuildConfig + apiVersion: build.openshift.io/v1 + metadata: + name: ruby-sample-build + labels: + name: ruby-sample-build + annotations: + template.alpha.openshift.io/wait-for-ready: "true" + spec: + triggers: + - type: GitHub + github: + secret: secret101 + - type: Generic + generic: + secret: secret101 + allowEnv: true + - type: ImageChange + imageChange: {} + - type: ConfigChange + source: + type: Git + git: + uri: https://github.com/openshift/ruby-hello-world.git + strategy: + type: Source + sourceStrategy: + from: + kind: ImageStreamTag + name: ruby-27:latest + env: + - name: EXAMPLE + value: sample-app + output: + to: + kind: ImageStreamTag + name: origin-ruby-sample:latest + postCommit: + script: bundle exec rake test + resources: {} + status: + lastVersion: 0 +- kind: Deployment + apiVersion: apps/v1 + metadata: + name: frontend + annotations: + template.alpha.openshift.io/wait-for-ready: "true" + image.openshift.io/triggers: '[{"from":{"kind":"ImageStreamTag","name":"origin-ruby-sample:latest"},"fieldPath": + "spec.template.spec.containers[0].image"}]' + spec: + strategy: + type: RollingUpdate + replicas: 2 + selector: + matchLabels: + name: frontend + template: + metadata: + labels: + name: frontend + icpdsupport/organizationId: "{{ .zenOrganizationId }}" + icpdsupport/dataPlaneId: "{{ .zenDataplaneId }}" + icpdsupport/dataPlaneName: "{{ .zenDataplaneName }}" + icpdsupport/physicalLocationId: "{{ .zenPhysicalLocationId }}" + icpdsupport/physicalLocationName: "{{ .zenPhysicalLocationName }}" + zenCloudPakInstanceId: "{{ .zenCloudPakInstanceId }}" + zenOrganizationName: "{{ .zenOrganizationName }}" + icpdata_run_id: '{{.RUN_ID}}' + spec: + containers: + - name: ruby-helloworld + image: origin-ruby-sample + ports: + - containerPort: 8080 + protocol: TCP + env: + - name: MYSQL_USER + valueFrom: + secretKeyRef: + name: dbsecret + key: mysql-user + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + name: dbsecret + key: mysql-password + - name: MYSQL_DATABASE + value: ${MYSQL_DATABASE} + resources: {} + terminationMessagePath: /dev/termination-log + imagePullPolicy: IfNotPresent + securityContext: + capabilities: {} + privileged: false + restartPolicy: Always + dnsPolicy: ClusterFirst +- kind: Service + apiVersion: v1 + metadata: + name: database + spec: + ports: + - name: db + protocol: TCP + port: 5434 + targetPort: 3306 + nodePort: 0 + selector: + name: database + type: ClusterIP + sessionAffinity: None + status: + loadBalancer: {} +- kind: Deployment + apiVersion: apps/v1 + metadata: + name: database + annotations: + template.alpha.openshift.io/wait-for-ready: "true" + spec: + strategy: + type: Recreate + replicas: 1 + selector: + matchLabels: + name: database + template: + metadata: + labels: + name: database + icpdsupport/organizationId: "{{ .zenOrganizationId }}" + icpdsupport/dataPlaneId: "{{ .zenDataplaneId }}" + icpdsupport/dataPlaneName: "{{ .zenDataplaneName }}" + icpdsupport/physicalLocationId: "{{ .zenPhysicalLocationId }}" + icpdsupport/physicalLocationName: "{{ .zenPhysicalLocationName }}" + zenCloudPakInstanceId: "{{ .zenCloudPakInstanceId }}" + zenOrganizationName: "{{ .zenOrganizationName }}" + icpdata_run_id: '{{.RUN_ID}}' + spec: + containers: + - name: ruby-helloworld-database + image: registry.redhat.io/rhel8/mysql-80:latest + ports: + - containerPort: 3306 + protocol: TCP + env: + - name: MYSQL_USER + valueFrom: + secretKeyRef: + name: dbsecret + key: mysql-user + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + name: dbsecret + key: mysql-password + - name: MYSQL_DATABASE + value: ${MYSQL_DATABASE} + resources: {} + volumeMounts: + - name: ruby-helloworld-data + mountPath: /var/lib/mysql/data + terminationMessagePath: /dev/termination-log + imagePullPolicy: Always + securityContext: + capabilities: {} + privileged: false + volumes: + - name: ruby-helloworld-data + emptyDir: + medium: "" + restartPolicy: Always + dnsPolicy: ClusterFirst +parameters: +- name: MYSQL_USER + description: database username + generate: expression + from: user[A-Z0-9]{3} + required: true +- name: MYSQL_PASSWORD + description: database password + generate: expression + from: '[a-zA-Z0-9]{8}' + required: true +- name: MYSQL_DATABASE + description: database name + value: root + required: true +labels: + template: application-template-stibuild \ No newline at end of file diff --git a/ruby-hello-world/ruby-hello-world.conf.yaml b/ruby-hello-world/ruby-hello-world.conf.yaml new file mode 100644 index 0000000..35bb352 --- /dev/null +++ b/ruby-hello-world/ruby-hello-world.conf.yaml @@ -0,0 +1,36 @@ +apiVersion: zen.cpd.ibm.com/v1 +kind: ZenExtension +metadata: + name: ruby-hello-world +spec: + extensions: | + [ + { + "extension_name": "ruby-hello-world", + "extension_type": "nginx", + "details": { + "upstream_conf": "upstream.conf", + "location_conf": "nginx.conf" + } + } + ] + upstream.conf: | + upstream ruby-hello-world { + keepalive 32; + keepalive_timeout 30s; + keepalive_requests 500; + server frontend.{{ .dpPhyLocWorkloadNs }}.svc:5432; + } + nginx.conf: | + location ~* /frontend/(.*) { + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Connection "Upgrade"; + proxy_set_header Upgrade $http_upgrade; + proxy_ssl_server_name on; + proxy_buffering off; + proxy_pass http://ruby-hello-world/$1$is_args$args; + proxy_pass_header X-Accel-Buffering; + sub_filter '/keys/' '/physical_location/{{ .zenPhysicalLocationName }}/frontend/keys/'; + sub_filter_once off; + } diff --git a/wxo-external-agent/README.md b/wxo-external-agent/README.md new file mode 100644 index 0000000..c677602 --- /dev/null +++ b/wxo-external-agent/README.md @@ -0,0 +1,60 @@ +# Deploy wxo adk external-agent to software hub dataplane + +[Wxo External Agent](watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/README.md) is an example app that provides chat completions api that can be used as an external agent in watson orchestrate, it interacts with ai service endpoints hosted on ibm cloud watsonx service. app is cloned from [here](https://github.com/watson-developer-cloud/watsonx-orchestrate-developer-toolkit/tree/main/external_agent/examples/langgraph_python) + +this is the instruction doc for deploying the app into software hub default dataplane: +- you need to have watsonx ai service/assets deployment in ibm cloud (refer to watsonx.ai documentation), you would need to collect: + - WATSONX_SPACE_ID: your ibm cloud deployment space for your ai service deployment + - WATSONX_API_KEY: your ibm cloud api key that has access to watsonx runtime service + +- checkout the [pre-requisites](../README.md#pre-requisites-to-deploy-sample-applications) required before deploying this application into default dataplane + +#### Steps to deploy ruby-hello-world +1. Change to the work directory inside the cpd-cli binary: + ``` + cd cpd-cli-workspace/olm-utils-workspace/work/ + ``` + +2. Create a new YAML file: + ``` + touch app-proxy-config.yaml + ``` +3. Copy the contents from the provided file in this folder and paste them into app-proxy-config.yaml. +4. Go back to the cpd-cli directory: + ``` + cd ../../.. + ``` +5. Run the create-dockerfile-application command: + ``` + ./cpd-cli manage create-dockerfile-application --instance_ns=zen --app_name=wxo-external-agent --app_port=8080 --app_port_tls=false --repo_url=https://github.com/IBM/swhub-custom-app-samples.git --repo_token= --repo_branch=main --repo_app_dir=wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python --app_envs='[{"name":"WATSONX_SPACE_ID","value":""},{"name":"WATSONX_API_KEY","value":""}]' --app_proxy_config_yaml='/tmp/work/app-proxy-config.yaml' --cpu=400m --memory=200Mi --cpu_limit=500m --memory_limit=400Mi + ``` +6. Check the pods in workload namespace + ``` + oc get po -n wl | grep external-agent + wxo-external-agent-h4yc75nkwi2w-1-build 0/1 Completed 0 4m2s + wxo-external-agent-h4yc75nkwi2w-7fdb57bb57-sx8vk 1/1 Running 0 2m25s + ``` +7. Update application proxy config - copy `app-proxy-config.yaml` in this folder to `cpd-cli-workspace/olm-utils-workspace/work/` + ``` + ./cpd-cli manage update-custom-application-proxy-config \ + --instance_ns=zen \ + --app_name=wxo-external-agent \ + --app_run_id= \ + --app_proxy_config_yaml=/tmp/work/app-proxy-config.yaml + ``` + +#### application available at `https://zen-route/physical_location/default-pl//docs` + +To try it out, use this request body: + + { + "model": "ibm/granite-3-3-8b-instruct", + "context": {}, + "messages": [ + { + "role": "assistant", + "content": "how to use foundational models?" + } + ], + "stream": false + } diff --git a/wxo-external-agent/app-proxy-config.yaml b/wxo-external-agent/app-proxy-config.yaml new file mode 100644 index 0000000..9431f96 --- /dev/null +++ b/wxo-external-agent/app-proxy-config.yaml @@ -0,0 +1,48 @@ +apiVersion: zen.cpd.ibm.com/v1 +kind: ZenExtension +metadata: + name: {{ .serviceName }} +spec: + extensions: | + [ + { + "extension_name": "{{ .serviceName }}", + "extension_type": "nginx", + "details": { + "upstream_conf": "upstream.conf", + "location_conf": "nginx.conf" + } + } + ] + upstream.conf: | + upstream {{ .upstreamName }} { + keepalive 32; + keepalive_timeout 30s; + keepalive_requests 500; + server {{ .serviceName }}.{{ .dpPhyLocWorkloadNs }}.svc:{{ .servicePort }}; + } + nginx.conf: | + location = /{{ .serviceName }}/openapi.json { + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Connection ""; + proxy_ssl_server_name on; + proxy_buffering off; + proxy_pass http://{{ .upstreamName }}/openapi.json; + proxy_pass_header X-Accel-Buffering; + sub_filter_once off; + sub_filter_types application/json; + sub_filter '"paths":{"/' '"paths":{"/physical_location/{{ .zenPhysicalLocationName }}/{{ .serviceName }}/'; + } + location ~* /{{ .serviceName }}/(.*) { + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Connection ""; + proxy_ssl_server_name on; + proxy_buffering off; + proxy_pass http://{{ .upstreamName }}/$1$is_args$args; + proxy_pass_header X-Accel-Buffering; + sub_filter '/openapi.json' '/physical_location/{{ .zenPhysicalLocationName }}/{{ .serviceName }}/openapi.json'; + sub_filter '/docs/' '/physical_location/{{ .zenPhysicalLocationName }}/{{ .serviceName }}/docs/'; + sub_filter_once off; + } diff --git a/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/README.md b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/README.md new file mode 100644 index 0000000..b2f7195 --- /dev/null +++ b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/README.md @@ -0,0 +1,71 @@ +# IBM watsonx orchestrate - external agent support + +For official feature documentation, refer to [link](https://developer.ibm.com/apis/catalog/watsonorchestrate--custom-assistants/api/API--watsonorchestrate--ibm-watsonx-orchestrate-api#Register_an_external_chat_completions_agent__agents_external_chat_post). + +## Starter Guide to Using the External Agent Feature + +For examples, check out the [examples](examples). + +### Create the Agent +Begin by creating an agent that exposes a chat streaming endpoint that emits the following events: + + +1. **Assistant response event** + +This event captures the agent response that will be shown to end user. For example: +``` +data: {"id": "run-8c1182", "object": "thread.message.delta", "thread_id": "c91e2e38-7b42-43d7-b913-0273951350a9", "model": "agent-02", "created": 1728566547, "choices": [{"delta": {"role": "assistant", "content": "Here are the upcoming holidays in the US:\n\n- National Coming Out Day: October 11, 2024\n- Yom Kippur: October 12, 2024\n- Columbus Day: October 14, 2024\n- Sukkot: October 17, 2024\n- Sweetest Day: October 19, 2024\n- National Cat Day: October 29, 2024\n- Halloween: October 31, 2024\n- Diwali: November 01, 2024\n\nSource:\n- [Holidays Calendar](https://www.holidayscalendar.com/countries/united-states/)\n- [Time and Date](https://www.timeanddate.com/holidays/us/)\n- [Federal Holidays](https://www.federalholidays.net/usa/federal-holidays-2024.html)\n- [Dayspedia](https://dayspedia.com/us/calendar/holidays/)"}}]} +``` + + +2. **Thinking step event** + +This event is optional. It's the intermediate thinking that an agent can produce. For example: +``` +data: {"id": "step-d08460", "object": "thread.run.step.delta", "thread_id": "c91e2e38-7b42-43d7-b913-0273951350a9", "model": "agent-02/mistralai/mistral-large-latest", "created": 1728566531, "choices": [{"delta": {"role": "assistant", "step_details": {"type": "thinking", "content": "The agent's response is a generic greeting and does not address the user's specific question about upcoming holidays in the US. I should use the available tools to find the relevant information and provide a more specific and helpful response."}}]} +``` + + +3. **Tool call event** + +This the event that captures an agent's tool calls. A single event can represent multiple tool calls. For example: +``` +data: {"id": "step-a0844e", "object": "thread.run.step.delta", "thread_id": "c91e2e38-7b42-43d7-b913-0273951350a9", "model": "agent-02/mistralai/mistral-large-latest", "created": 1728566532, "choices": [{"delta": {"role": "assistant", "step_details": {"type": "tool_calls", "tool_calls": [{"name": "find_employee_by_name", "args": {"name": "vsingh"}, "id": "e32ff32a4"}]}}]} +``` + +Note: In the tool call array `"tool_calls": [{"name": "find_employee_by_name", "args": {"name": "vsingh"}, "id": "e32ff32a4"}]`, each tool_call can have a unique id like `e32ff32a4`. +This id is used to match with Tool response event. + +4. **Tool response event** + +This the event that captures an agent's tool response output. Each event represents the output from a single tool call. For example: +``` +data: {"id": "step-03050c", "object": "thread.run.step.delta", "thread_id": "ae1b353f-65b9-4668-b1e8-9298979c9697", "model": "agent-02", "created": 1729091694, "choices": [{"delta": {"role": "assistant", "step_details": {"type": "tool_response", "content": "[{'cellPhone': None, 'city': None, 'country': 'India', 'dateOfBirth': datetime.date(1998, 6, 30), 'defaultFullName': 'Vijaya Singh', 'department': 'HR Total Rewards (5000145)', 'email': 'kota.manaswini@partner.ibm.com', 'firstName': 'Vijaya', 'gender': ' ', 'hireDate': datetime.date(2023, 6, 30), 'lastName': 'Singh', 'salary': None, 'state': None, 'title': 'Digital Consultant', 'userId': '103451', 'businessPhone': None, 'location': 'Hyderabad (6200-0005)'}]", "name": "find_employee_by_name", "tool_call_id": "e32ff32a4"}}}]} +``` + + +Note: In this example, the `event.choices[0].delta.step_details.tool_call_id` needs to match the corresponding `tool_calls[0].id` in `event.choices[0].delta.step_details.tool_calls[0].id`. +The `tool_call_id` of the `tool_response` being present and matching the `id` of the `tool_call` will be required for the Watsonx Orchestrate UX to render Chain of Thoughts correctly. + + +5. **End of a single call** + +At the end of the stream, emit the following: +``` +data: [DONE] +``` + +For more details regarding input & output specifications, refer to the [provided specifications](spec.yaml). + + +### Implement Authentication +The agent must support authentication through either of the following methods: + +1. Bearer Token Authentication +2. API Key Authentication using an `x-api-key` header. + +### Deploy the Agent +Once the agent is ready, deploy it externally (e.g., using [code engine](https://www.ibm.com/products/code-engine)). + +### Register the Agent +After deployment, register the agent according to the instructions provided in our product documentation. \ No newline at end of file diff --git a/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/Dockerfile b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/Dockerfile new file mode 100644 index 0000000..fa2c34a --- /dev/null +++ b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.11-slim + +WORKDIR /app +RUN chmod 775 /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/README.md b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/README.md new file mode 100644 index 0000000..ac2c283 --- /dev/null +++ b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/README.md @@ -0,0 +1,95 @@ +# IBM Watsonx Orchestrate - External Agent Example + +Use these examples to kick-start your external agent development. + +For official feature documentation, refer to the [IBM Developer API Catalog](https://developer.ibm.com/apis/catalog/watsonorchestrate--custom-assistants/api/API--watsonorchestrate--ibm-watsonx-orchestrate-api#Register_an_external_chat_completions_agent__agents_external_chat_post). + +## Overview + +This example demonstrates how to deploy an external agent as a serverless application in IBM Cloud. The application leverages +FastAPI and [LangGraph](https://www.langchain.com/langgraph) to create a chat completion service that integrates with IBM watsonx and OpenAI models. It also includes AI tools for web and news searches using DuckDuckGo. + +## Video +[Video walking through this example](./wxo_external_langgraph_agent_demo.mp4) + +## Features + +- **Chat Completion Service**: The application provides a RESTful API endpoint for chat completions, supporting both synchronous and streaming responses following the specification of IBM Orchestrate external agents. +- **Integration with AI Models**: It provides an example that supports multiple AI models, including IBM's watsonx and OpenAI's GPT, allowing for flexible AI-driven interactions. +- **Tool Integration**: The application includes tools for web and news searches using DuckDuckGo, which can be invoked during chat interactions. +- **Token Management**: Implements a caching mechanism for IBM Cloud IAM tokens to optimize authentication processes. +- **Logging and Debugging**: Logging is set up to facilitate debugging and monitoring of the application. + +## Security Limitations + +Please be aware that this example accepts any API Key or Bearer token for authentication. +It is recommended to implement your own authentication security measures to ensure proper security. + +## Deployment Instructions + +### Step 1: Create a Code Engine Project + +1. **Using IBM Cloud Web UI:** + - Navigate to [IBM Cloud Code Engine Projects](https://cloud.ibm.com/containers/serverless/projects) and select **Create**. Name your project, for instance `wxo-agent-test1`. + - Select the agent you created (`wxo-agent-test1`) and choose the **Application** menu item from the left navigation panel. + +2. **Create an API Key for Registry Secret:** + - Select **Manage** from the title bar menu and go to **Access (IAM)**. + - From the left navigation menu, select **API keys**. + - Click **Create** and copy the new API key for use in the registry secret. + +3. **Create the Code Engine Application:** + - Click the **Create** button to start creating an application. + - Under **Code**, select **Build container image from source code**. + - In the **Code repo URL** field, enter `https://github.com/watson-developer-cloud/watsonx-orchestrate-developer-toolkit`. + - Click **Specify build details**: + - **SSH secret:** None + - **Branch name:** main + - **Context directory:** `external_agent/examples/langgraph_python` + - Click **Next** + - **Dockerfile:** Dockerfile (leave default) + - Click **Next** + - Under **Registry secret**, create a secret (if one doesn't exist) using the **API Key** created above + - **Application name:** Any name, for instance `wxo-agent-test1-app1` + - **Domain mappings:** Public + +4. **Set Environment Variables:** + - Add the following environment variables: + - `WATSONX_SPACE_ID` or `WATSONX_PROJECT_ID` + - `WATSONX_API_KEY` + - `OPENAI_API_KEY` (only needed if you plan to use OpenAI models) + - Select the `Create` button + +5. **Test the Application:** + - Choose **Test application** and click **Application URL**. + - It is expected this page will not be found, we need to slightly update the path + - Append `/docs` to the end of the URL path to view a formatted API page. + - Example: `https://wxo-agent-test1-app1.1pj4w3r1pi47.us-south.codeengine.appdomain.cloud/docs` + +### Step 2: Register the New Endpoint as an External Agent + +1. **In IBM watsonx orchestrate Web UI:** + - From the top left hamburger menu, select **Agent Configuration**. + - Select **Assistants** from the left-hand navigation. + - Click the **Add assistant** button on the top right. + - Choose **External Assistant** at the top of the dialog. + - Check the **External-agent Assistant** box. + +2. **Enter Details:** + - **Display Name:** e.g., News Today + - **Description:** Enter a description of capabilities, for instance `Agent to retieve current news` + - **API Key:** Enter your API key. + - **Service Instance URL:** Use the Test URL with `/chat/completions` appended. + - Example: `https://wxo-agent-test1-app1.1pj4w3r1pi47.us-south.codeengine.appdomain.cloud/chat/completions` + +![Alt text](./register_wxo_external_agent.png "Example of registering an external agent to IBM watsonx Orchestrate") + +### Step 3: Call the new External Agent from Orchestrate + +1. **In IBM watsonx orchestrate Web UI:** + - From the top left hamburger menu, select **Agent Configuration**. + - Select **Chat** from the left-hand navigation. + - Type a question that should route to the new agent, like `Can you tell me news about the Nasdaq today?` + - The results from the external agent should be streamed to the IBM watsonx Orchestrate chat window + +![Alt text](./chat_external_agent.png "Example of a chat to the external agent from IBM watsonx Orchestrate") \ No newline at end of file diff --git a/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/app.py b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/app.py new file mode 100644 index 0000000..4816184 --- /dev/null +++ b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/app.py @@ -0,0 +1,64 @@ +import logging +import uuid +import time +from typing import Optional, Dict, Any +from fastapi import FastAPI, Header, Depends +from fastapi.responses import JSONResponse, StreamingResponse +from models import ChatCompletionRequest, ChatCompletionResponse, Choice, MessageResponse, DEFAULT_MODEL +from security import get_current_user +from tools import web_search_duckduckgo, news_search_duckduckgo +from llm_utils import get_llm_sync, get_llm_stream + +logger = logging.getLogger() +logger.setLevel(logging.INFO) +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + +app = FastAPI() + +@app.post("/chat/completions") +async def chat_completions( + request: ChatCompletionRequest, + X_IBM_THREAD_ID: Optional[str] = Header(None, alias="X-IBM-THREAD-ID", description="Optional header to specify the thread ID"), + current_user: Dict[str, Any] = Depends(get_current_user), +): + logger.info(f"Received POST /chat/completions ChatCompletionRequest: {request.json()}") + thread_id = '' + if X_IBM_THREAD_ID: + thread_id = X_IBM_THREAD_ID + if request.extra_body and request.extra_body.thread_id: + thread_id = request.extra_body.thread_id + logger.info("thread_id: " + thread_id) + model = DEFAULT_MODEL + if request.model: + model = request.model + selected_tools = [web_search_duckduckgo, news_search_duckduckgo] + if request.stream: + return StreamingResponse(get_llm_stream(request.messages, model, thread_id, selected_tools), media_type="text/event-stream") + else: + last_message, all_messages = get_llm_sync(request.messages, model, thread_id, selected_tools) + id = str(uuid.uuid4()) + response = ChatCompletionResponse( + id=id, + object="chat.completion", + created=int(time.time()), + model=request.model, + choices=[ + Choice( + index=0, + message=MessageResponse( + role="assistant", + content=last_message + ), + finish_reason="stop" + ) + ] + ) + return JSONResponse(content=response.dict()) + +if __name__ == '__main__': + import uvicorn + uvicorn.run(app, host='0.0.0.0', port=8080) diff --git a/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/chat_external_agent.png b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/chat_external_agent.png new file mode 100644 index 0000000..64fef65 Binary files /dev/null and b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/chat_external_agent.png differ diff --git a/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/config.py b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/config.py new file mode 100644 index 0000000..5aa9379 --- /dev/null +++ b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/config.py @@ -0,0 +1,7 @@ +import os + +WATSONX_SPACE_ID = os.getenv('WATSONX_SPACE_ID', None) +WATSONX_PROJECT_ID = os.getenv('WATSONX_PROJECT_ID', None) +WATSONX_API_KEY = os.getenv('WATSONX_API_KEY', None) +WATSONX_URL = os.getenv('WATSONX_URL','https://us-south.ml.cloud.ibm.com') +OPENAI_API_KEY = os.getenv('OPENAI_API_KEY', None) diff --git a/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/llm_utils.py b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/llm_utils.py new file mode 100644 index 0000000..4b0fc08 --- /dev/null +++ b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/llm_utils.py @@ -0,0 +1,376 @@ +import time +import json +import uuid +import traceback +import logging +from typing import List, Dict, Any, Optional +from langchain_openai import ChatOpenAI +from langchain_core.messages import HumanMessage, AIMessage, ToolMessage, SystemMessage, BaseMessage, ToolCall +from langgraph.prebuilt import create_react_agent +from ibm_watsonx_ai import APIClient, Credentials +from langchain_ibm import ChatWatsonx +from models import Message, AIToolCall, Function, ChatCompletionResponse, Choice, MessageResponse +from config import OPENAI_API_KEY, WATSONX_SPACE_ID, WATSONX_API_KEY, WATSONX_URL, WATSONX_PROJECT_ID +from token_utils import get_access_token + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +def init_openai(model: str, parm_overrides: dict = {}): + defaults = { + 'temperature': 0, + 'streaming': False + } + defaults.update(parm_overrides) + return ChatOpenAI(model=model, **defaults) + +def convert_messages_to_langgraph_format(messages: List[Message]) -> Dict[str, Any]: + conv_messages = [] + max_message_length = 50000 + for msg in messages: + if msg.content and len(msg.content) > max_message_length: + msg.content = msg.content[:max_message_length] + role = msg.role + logger.debug(f"Converting input message of type {role}") + if role.lower() == 'user' or role.lower() == 'human': + new_message = HumanMessage(content=msg.content) + if role.lower() == 'system': + new_message = SystemMessage(content=msg.content) + if role.lower() == 'assistant': + content = '' + additional_kwargs = {} + if msg.content: + content = msg.content + if msg.tool_calls: + # Convert list of AIToolCall messages to langchain ToolCall message + langchain_tool_calls = [] + for index, tool_call in enumerate(msg.tool_calls): + name = tool_call.function.name + args = tool_call.function.arguments + id = tool_call.id + langchain_tool_calls.append(ToolCall(name=name, args=args, id=id, type='tool')) + + new_message = AIMessage(content=content, tool_calls=langchain_tool_calls, additional_kwargs=additional_kwargs) + else: + new_message = AIMessage(content=content, additional_kwargs=additional_kwargs) + if role.lower() == 'tool': + tool_call_id = msg.tool_call_id + content = msg.content + name = None + new_message = ToolMessage(content=content, name=name, tool_call_id=tool_call_id) + conv_messages.append(new_message) + return { + "messages": conv_messages + } + +def convert_response_to_messages(response: dict) -> List[Message]: + messages = [] + for msg in response['messages']: + role = 'not found' + if msg.type: + role = msg.type + logger.info(f"Processing role {role}") + tool_calls = None + if 'tool_calls' in msg: + tool_calls = msg['tool_calls'] + if msg.additional_kwargs: + additional_kwargs = msg.additional_kwargs + if 'tool_calls' in additional_kwargs: + tool_calls = [] + for tool_call_data in additional_kwargs['tool_calls']: + function_arguments = tool_call_data['function']['arguments'] + if isinstance(function_arguments, str): + function_arguments = json.loads(function_arguments) + tool_call = AIToolCall( + id=tool_call_data['id'], + function=Function( + arguments=function_arguments, + name=tool_call_data['function']['name'] + ), + type=tool_call_data['type'] + ) + tool_calls.append(tool_call) + content = "" + if msg.content: + content = msg.content + id = None + if msg.id: + id = msg.id + name = None + if 'name' in msg: + name = msg['name'] + if msg.name: + name = msg.name + tool_call_id = None + if 'tool_call_id' in msg: + tool_call_id = msg['tool_call_id'] + if role == 'tool' and msg.tool_call_id: + tool_call_id = msg.tool_call_id + if role == 'human': + message = Message( + role='user', + content=content + ) + elif role == 'ai': + message = Message( + role='assistant', + content=content + ) + else: + message = Message( + role=role, + content=content + ) + messages.append(message) + return messages + +def get_llm_sync(messages: List[Message], model: str, thread_id: str, tools): + logger.info(f"LLM Synchronous call using model {model} and tools {tools}") + model_instance = None + if 'gpt' in model: + if not OPENAI_API_KEY: + return "API key not set\n" + model_instance = init_openai(model, {}) + else: + client_model_instance = None + if WATSONX_SPACE_ID: + client_model_instance = APIClient(credentials=Credentials(url=WATSONX_URL, token=get_access_token(WATSONX_API_KEY)), + space_id=WATSONX_SPACE_ID) + elif WATSONX_PROJECT_ID: + client_model_instance = APIClient(credentials=Credentials(url=WATSONX_URL, token=get_access_token(WATSONX_API_KEY)), + project_id=WATSONX_PROJECT_ID) + else: + logger.error("You must either set WATSONX_SPACE_ID or WATSONX_PROJECT_ID") + model_instance = ChatWatsonx(model_id=model, watsonx_client=client_model_instance) + logger.info(f"Starting with input messages: {messages}") + inputs = convert_messages_to_langgraph_format(messages) + validate_chat_history(inputs["messages"]) + logger.info(f"Calling langgraph with input: {inputs}") + if tools: + graph = create_react_agent(model_instance, tools=tools) + response = graph.invoke(inputs) + else: + graph = model_instance + response = graph.invoke(inputs['messages']) + logger.info(f"Response: {response}") + if hasattr(response, 'content'): + results = response.content + message = Message( + role='ai', + content=results + ) + response_messages = [message.dict()] + else: + results = response["messages"][-1].content + return results, messages + +def format_resp(struct): + return "data: " + json.dumps(struct) + "\n\n" + +def validate_chat_history(messages: List[BaseMessage]): + tool_call_ids = set() + for msg in messages: + if isinstance(msg, AIMessage) and msg.tool_calls: + for tool_call in msg.tool_calls: + if isinstance(tool_call, dict): + tool_call_ids.add(tool_call.get('id')) + else: + tool_call_ids.add(tool_call.id) + + for msg in messages: + if isinstance(msg, ToolMessage): + if msg.tool_call_id in tool_call_ids: + tool_call_ids.remove(msg.tool_call_id) + + for tool_call_id in tool_call_ids: + logger.info(f"Fixing input that had no tool response for tool_call_id {tool_call_id}") + placeholder_message = ToolMessage( + content="Tool call failed or no response received.", + tool_call_id=tool_call_id, + name="unknown" + ) + messages.append(placeholder_message) + +async def get_llm_stream(messages: List[Message], model: str, thread_id: str, tools): + if tools: + use_tools = True + else: + use_tools = False + send_tool_events = True + logger.info(f"LLM Stream with tools {tools}") + model_init_overrides = {'temperature': 0, 'streaming': True} + if not thread_id: + logger.warn("Warning no thread_id specified in input") + thread_id = "" + if 'gpt' in model: + if not OPENAI_API_KEY: + yield "API key not set\n" + model_instance = init_openai(model, model_init_overrides) + else: + client_model_instance = None + if WATSONX_SPACE_ID: + client_model_instance = APIClient(credentials=Credentials(url=WATSONX_URL, token=get_access_token(WATSONX_API_KEY)), + space_id=WATSONX_SPACE_ID) + elif WATSONX_PROJECT_ID: + client_model_instance = APIClient(credentials=Credentials(url=WATSONX_URL, token=get_access_token(WATSONX_API_KEY)), + project_id=WATSONX_PROJECT_ID) + else: + logger.error("You must either set WATSONX_SPACE_ID or WATSONX_PROJECT_ID") + model_instance = ChatWatsonx(model_id=model, watsonx_client=client_model_instance) + if use_tools: + graph = create_react_agent(model_instance, tools=tools) + else: + graph = create_react_agent(model_instance, tools=[]) + inputs = "" + accumulated_contents = "" + try: + inputs = convert_messages_to_langgraph_format(messages) + validate_chat_history(inputs["messages"]) + async for event in graph.astream_events(inputs, version="v2"): + kind = event["event"] + logger.debug(f"event = {event}") + if kind == "on_chat_model_stream": + content = event["data"]["chunk"].content + if content: + if isinstance(content, str): + current_timestamp = int(time.time()) + struct = { + "id": str(uuid.uuid4()), + "object": "thread.message.delta", + "created": current_timestamp, + "thread_id": thread_id, + "model": model, + "choices": [ + { + "delta": { + "content": content, + "role": "assistant", + } + } + ], + } + event_content = format_resp(struct) + logger.debug("Sending event content: " + event_content) + accumulated_contents += content + yield event_content + elif isinstance(content, list): + for item in content: + if 'type' in item: + if item['type'] == 'text': + yield item['text'] + elif item['type'] == 'tool_use': + logger.debug("tool_use") + logger.debug(f"{str(item)}") + else: + logger.debug("Received item of type " + item['type']) + elif kind == "on_tool_start": + printmsg = f"Starting tool: {event['name']} with inputs: {event['data'].get('input')} run_id: {event['run_id']}" + logger.debug(printmsg) + current_timestamp = int(time.time()) + step_details = { + "type": "tool_calls", + "tool_calls": [ + { + "id": event['run_id'], + "name": event['name'], + "args": event['data'].get('input') + } + ] + } + struct = { + "id": str(uuid.uuid4()), + "object": "thread.run.step.delta", + "thread_id": thread_id, + "model": model, + "created": current_timestamp, + "choices": [ + { + "delta": { + "role": "assistant", + "step_details": step_details + } + } + ], + } + thinking_step_details = { + "type": "thinking", + "content": "The user's question will require an internet search using a search tool." + } + thinking_struct = { + "id": str(uuid.uuid4()), + "object": "thread.run.step.delta", + "thread_id": thread_id, + "model": model, + "created": current_timestamp, + "choices": [ + { + "delta": { + "role": "assistant", + "step_details": thinking_step_details + } + } + ] + } + thinking_event_content = format_resp(thinking_struct) + logger.info("Sending thinking event content: " + thinking_event_content) + if send_tool_events: + yield thinking_event_content + event_content = format_resp(struct) + logger.info("Sending tool call event content: " + event_content) + if send_tool_events: + yield event_content + elif kind == "on_tool_end": + tool_name = event.get('name', '') + logger.info(f"Event on_tool_end for tool: {tool_name}") + output = event.get('data', {}).get('output', {}) + content = '' + if output and output.content: + content = output.content + run_id = event['run_id'] + logger.info(f"Tool output for run {run_id} was: {content}") + tool_call_id = '' + if output and output.tool_call_id: + tool_call_id = output.tool_call_id + tool_call_id = run_id #Better matches tool response with tool request + current_timestamp = int(time.time()) + step_details = { + "type": "tool_response", + "name": event['name'], + "tool_call_id": tool_call_id, + "content": content + } + struct = { + "id": str(uuid.uuid4()), + "object": "thread.run.step.delta", + "thread_id": thread_id, + "model": model, + "created": current_timestamp, + "choices": [ + { + "delta": { + "role": "assistant", + "step_details": step_details + } + } + ], + } + event_content = format_resp(struct) + logger.info("Sending tool response event content: " + event_content) + if send_tool_events: + yield event_content + elif kind == "on_chat_model_start": + logger.debug(f"Received event type: on_chat_model_start") + elif kind == "on_chat_model_end": + logger.debug(f"Received event type: on_chat_model_end") + else: + logger.debug("Received event type: " + kind) + yield "" + + if accumulated_contents: + logger.info("Final streamed content:\n" + accumulated_contents) + + except Exception as e: + logger.error(f"Exception {str(e)}") + traceback.print_exc() + logger.error(f"Exception was with inputs {str(inputs)}") + yield f"Error: {str(e)}\n" diff --git a/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/models.py b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/models.py new file mode 100644 index 0000000..c09847c --- /dev/null +++ b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/models.py @@ -0,0 +1,80 @@ +from pydantic import BaseModel, Field, field_validator +from enum import Enum +from typing import List, Optional, Dict, Any +import json + +class ModelName(str, Enum): + mistral_large = "mistralai/mistral-large" + llama_3_1_405b = "meta-llama/llama-3-405b-instruct" + llama_3_2_90b = "meta-llama/llama-3-2-90b-vision-instruct" + gpt_4_o_mini = "gpt-4o-mini" + +class ToolName(str, Enum): + web_search_duckduckgo = "web_search_duckduckgo" + news_search_duckduckgo = "news_search_duckduckgo" + +DEFAULT_MODEL=ModelName.llama_3_2_90b + +class Function(BaseModel): + name: str + arguments: Dict[str, Any] + + @field_validator('arguments', mode='before') + def check_arguments(cls, value): + # Detect if arguments are passed as a string + if isinstance(value, str): + try: + # Attempt to parse stringified JSON + value = json.loads(value) + except json.JSONDecodeError: + raise ValueError("Invalid JSON format for arguments") + return value + +class AIToolCall(BaseModel): + id: str + function: Function + type: str + +class AIRESTMessage(BaseModel): + role: str + content: Optional[str] = None + tool_calls: Optional[List[AIToolCall]] = None + name: Optional[str] = None + tool_call_id: Optional[str] = None + def to_clean_dict(self): + return {k: v for k, v in self.dict().items() if v is not None} + +class Message(BaseModel): + role: str = Field(..., description="The role of the message sender", pattern="^(user|assistant|system|tool)$") + content: Optional[str] = Field(None, description="The content of the message. It can be null if no content is provided.") + tool_calls: Optional[List[AIToolCall]] = Field(None, description="List of tool calls, if applicable.") + tool_call_id: Optional[str] = Field(None, description="Tool call id if role is tool. It can be null if no content is provided.") + +class ExtraBody(BaseModel): + thread_id: Optional[str] = Field(None, description="The thread ID for tracking the request") + +class ChatCompletionRequest(BaseModel): + model: Optional[str] = Field( + default=DEFAULT_MODEL, + description="ID of the model to use. If not provided, a default model will be used" + ) + context: Dict[str, Any] = Field({}, description="Contextual information for the request") + messages: List[Message] = Field(..., description="List of messages in the conversation") + stream: Optional[bool] = Field(False, description="Whether to stream responses as server-sent events") + extra_body: Optional[ExtraBody] = Field(None, description="Additional data or parameters") + +class MessageResponse(BaseModel): + role: str = Field(..., description="The role of the message sender", pattern="^(user|assistant)$") + content: str = Field(..., description="The content of the message") + +class Choice(BaseModel): + index: int = Field(..., description="The index of the choice") + message: MessageResponse = Field(..., description="The message") + finish_reason: Optional[str] = Field(None, description="The reason the message generation finished") + +class ChatCompletionResponse(BaseModel): + id: str = Field(..., description="Unique identifier for the completion") + object: str = Field("chat.completion", description="The type of object returned, should be 'chat.completion'") + created: int = Field(..., description="Timestamp of when the completion was created") + model: str = Field(..., description="The model used for generating the completion") + choices: List[Choice] = Field(..., description="List of completion choices") diff --git a/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/register_wxo_external_agent.png b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/register_wxo_external_agent.png new file mode 100644 index 0000000..92c5c0f Binary files /dev/null and b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/register_wxo_external_agent.png differ diff --git a/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/requirements.txt b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/requirements.txt new file mode 100644 index 0000000..e5c57f6 --- /dev/null +++ b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/requirements.txt @@ -0,0 +1,12 @@ +fastapi +uvicorn +python-dotenv +langgraph +langchain_anthropic +tavily-python +langchain-community +langchain_openai +langchain-ibm +duckduckgo-search +ddgs +ibm-watsonx-ai diff --git a/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/security.py b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/security.py new file mode 100644 index 0000000..c60c712 --- /dev/null +++ b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/security.py @@ -0,0 +1,19 @@ +from fastapi import Depends +from typing import Optional, Dict, Any +from fastapi.security import APIKeyHeader, HTTPBearer, HTTPAuthorizationCredentials + +#This example allows any bearer or api keys to be valid +api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) +http_bearer = HTTPBearer(auto_error=False) + +async def get_api_key(api_key_header: str = Depends(api_key_header)) -> Optional[str]: + return api_key_header + +async def get_bearer_token(credentials: HTTPAuthorizationCredentials = Depends(http_bearer)) -> Optional[str]: + return credentials.credentials if credentials else None + +async def get_current_user( + api_key: Optional[str] = Depends(get_api_key), + token: Optional[str] = Depends(get_bearer_token) +) -> Dict[str, Any]: + return {"api_key": api_key, "token": token} diff --git a/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/token_utils.py b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/token_utils.py new file mode 100644 index 0000000..a808793 --- /dev/null +++ b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/token_utils.py @@ -0,0 +1,33 @@ +import os +import time +import requests +import json + +def get_access_token(WATSONX_API_KEY): + api_key = WATSONX_API_KEY + file_path = './current_token.txt' + url = "https://iam.cloud.ibm.com/identity/token" + headers = {'content-type': 'application/x-www-form-urlencoded', + 'accept': 'application/json'} + data = {'grant_type': 'urn:ibm:params:oauth:grant-type:apikey', + 'apikey': api_key} + + if os.path.isfile(file_path): + file_time = os.path.getmtime(file_path) + if time.time() - file_time < 3600: + print("Retrieved cached token ") + with open(file_path, "r") as file: + return file.read() + + response = requests.post(url, headers=headers, data=data) + + if response.status_code == 200: + token_data = json.loads(response.text) + token = token_data["access_token"] + + with open(file_path, "w") as file: + file.write(token) + print("Retrieved new token for ") + return token + else: + raise Exception("Failed to get access token") diff --git a/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/tools.py b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/tools.py new file mode 100644 index 0000000..804709c --- /dev/null +++ b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/tools.py @@ -0,0 +1,21 @@ +from langchain_core.tools import tool +from langchain_community.tools import DuckDuckGoSearchResults + +@tool +def web_search_duckduckgo(search_phrase: str): + """Search the web using duckduckgo.""" + search = DuckDuckGoSearchResults() + results = search.run(search_phrase) + return results + +@tool +def news_search_duckduckgo(search_phrase: str): + """Search news using duckduckgo.""" + search = DuckDuckGoSearchResults(backend="news") + results = search.run(search_phrase) + return results + +tool_choices = { + "web_search_duckduckgo": web_search_duckduckgo, + "news_search_duckduckgo": news_search_duckduckgo, +} diff --git a/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/wxo_external_langgraph_agent_demo.mp4 b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/wxo_external_langgraph_agent_demo.mp4 new file mode 100644 index 0000000..5fc6b70 Binary files /dev/null and b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/examples/langgraph_python/wxo_external_langgraph_agent_demo.mp4 differ diff --git a/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/spec.yaml b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/spec.yaml new file mode 100644 index 0000000..dff0d3e --- /dev/null +++ b/wxo-external-agent/watsonx-orchestrate-developer-toolkit/external_agent/spec.yaml @@ -0,0 +1,212 @@ +openapi: 3.0.0 +info: + title: Agent Chat API + description: API for interacting with an agent-based chat system, allowing clients to send messages, receive responses, and manage chat sessions. + version: 1.0.0 + +servers: + - url: https://api.agentchat.example.com/v1 + description: The agent server endpoint + +paths: + /chat/completions: + post: + summary: Generate chat completions + description: Generate a response from the agent based on the user input, similar to the OpenAI chat completions endpoint. + parameters: + - name: X-IBM-THREAD-ID + in: header + required: false + schema: + type: string + description: Optional header to specify the thread ID + tags: + - Agent Chat + operationId: chat + requestBody: + content: + application/json: + schema: + type: object + properties: + model: + type: string + description: ID of the model to use + messages: + type: array + items: + type: object + properties: + role: + type: string + enum: [user, assistant, system, tool] + description: The role of the message sender + content: + type: string + description: The content of the message + stream: + type: boolean + description: Whether to stream responses as server-sent events + required: + - model + - messages + security: + - HTTPBearer: [] + - ApiKeyAuth: [] + responses: + '200': + description: Chat completion response + content: + application/json: + schema: + type: object + properties: + id: + type: string + description: Unique identifier for the completion + object: + type: string + description: The type of object returned, should be 'chat.completion' + created: + type: integer + description: Timestamp of when the completion was created + model: + type: string + description: The model used for generating the completion + choices: + type: array + items: + type: object + properties: + index: + type: integer + description: The index of the choice + message: + type: object + properties: + role: + type: string + enum: [user, assistant] + description: The role of the message sender + content: + type: string + description: The content of the message + finish_reason: + type: string + description: The reason the message generation finished + text/event-stream: + schema: + type: string + example: | + event: thread.message.delta + data: {"id": "run-8c1182", "object": "thread.message.delta", "thread_id": "c91e2e38-7b42-43d7-b913-0273951350a9", "model": "agent-02", "created": 1728566547, "choices": [{"delta": {"role": "assistant", "content": "Here are the upcoming holidays in the US:\n\n- National Coming Out Day: October 11, 2024\n- Yom Kippur: October 12, 2024\n- Columbus Day: October 14, 2024\n- Sukkot: October 17, 2024\n- Sweetest Day: October 19, 2024\n- National Cat Day: October 29, 2024\n- Halloween: October 31, 2024\n- Diwali: November 01, 2024\n\nSource:\n- [Holidays Calendar](https://www.holidayscalendar.com/countries/united-states/)\n- [Time and Date](https://www.timeanddate.com/holidays/us/)\n- [Federal Holidays](https://www.federalholidays.net/usa/federal-holidays-2024.html)\n- [Dayspedia](https://dayspedia.com/us/calendar/holidays/)"}}]} + + event: thread.run.step.delta + data: {"id": "step-d08460", "object": "thread.run.step.delta", "thread_id": "c91e2e38-7b42-43d7-b913-0273951350a9", "model": "agent-02/mistralai/mistral-large-latest", "created": 1728566531, "choices": [{"delta": {"role": "assistant", "step_details": {"type": "thinking", "content": "The agent's response is a generic greeting and does not address the user's specific question about upcoming holidays in the US. I should use the available tools to find the relevant information and provide a more specific and helpful response."}}]} + + event: thread.run.step.delta + data: {"id": "step-a0844e", "object": "thread.run.step.delta", "thread_id": "c91e2e38-7b42-43d7-b913-0273951350a9", "model": "agent-02/mistralai/mistral-large-latest", "created": 1728566532, "choices": [{"delta": {"role": "assistant", "step_details": {"type": "tool_calls", "tool_calls": [{"name": "find_employee_by_name", "args": {"name": "vsingh"}, "id": "e32ff32a4"}]}}]} + + event: thread.run.step.delta + data: {"id": "step-03050c", "object": "thread.run.step.delta", "thread_id": "ae1b353f-65b9-4668-b1e8-9298979c9697", "model": "agent-02", "created": 1729091694, "choices": [{"delta": {"role": "assistant", "step_details": {"type": "tool_response", "content": "[{'cellPhone': None, 'city': None, 'country': 'India', 'dateOfBirth': datetime.date(1998, 6, 30), 'defaultFullName': 'Vijaya Singh', 'department': 'HR Total Rewards (5000145)', 'email': 'kota.manaswini@partner.ibm.com', 'firstName': 'Vijaya', 'gender': ' ', 'hireDate': datetime.date(2023, 6, 30), 'lastName': 'Singh', 'salary': None, 'state': None, 'title': 'Digital Consultant', 'userId': '103451', 'businessPhone': None, 'location': 'Hyderabad (6200-0005)'}]", "name": "find_employee_by_name", "tool_call_id": "e32ff32a4"}}}]} + +components: + schemas: + ThreadMessageDelta: + type: object + properties: + id: + type: string + description: Unique identifier for the event + object: + type: string + description: The type of object returned, should be 'thread.message.delta' + thread_id: + type: string + description: Unique identifier for the thread + model: + type: string + description: The model used for generating the delta message + created: + type: integer + description: Timestamp of when the delta message was created + choices: + type: array + items: + type: object + properties: + delta: + type: object + properties: + role: + type: string + enum: [user, assistant] + description: The role of the message sender + content: + type: string + description: The content of the delta message + ThreadRunStepDelta: + type: object + properties: + id: + type: string + description: Unique identifier for the event + object: + type: string + enum: + - 'thread.run.step.delta' + - 'thread.run.step.created' + - 'thread.run.step.completed' + thread_id: + type: string + description: Unique identifier for the thread + model: + type: string + description: The model used for generating the step delta + created: + type: integer + description: Timestamp of when the step delta was created + choices: + type: array + items: + type: object + properties: + delta: + type: object + properties: + role: + type: string + enum: [user, assistant] + description: The role of the step action + step_details: + type: object + properties: + type: + type: string + enum: [thinking, tool_calls, tool_response] + description: The type of step being performed + content: + type: string + description: Additional context for the step + tool_calls: + type: array + items: + type: object + properties: + id: + type: string + description: Unique identifier for the tool call + name: + type: string + description: The name of the tool being called + args: + type: object + description: Arguments provided to the tool + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: X-API-Key + HTTPBearer: + type: http + scheme: bearer \ No newline at end of file