From f755fc467c5f8bbc31a8f01f3a6409f6dd4db1fc Mon Sep 17 00:00:00 2001 From: Victor Algaze Date: Mon, 8 Jul 2024 22:06:46 -0700 Subject: [PATCH] add azure sample, implement various typo fixes --- docs/.vitepress/components/monaco.vue | 60 +- docs/.vitepress/components/token_handler.vue | 3 + docs/.vitepress/config.mts | 13 + docs/.vitepress/util/store.ts | 5 + docs/api-docs/README.md | 46 +- docs/api-docs/classes/SpeedyBot.md | 102 +-- docs/api-docs/classes/SpeedyCard.md | 60 +- docs/api-docs/modules.md | 2 +- docs/examples/azure/README.md | 342 +++++++++ docs/examples/standard-server/README.md | 4 +- docs/new.md | 36 +- examples/azure/.env.example | 13 + examples/azure/README.md | 94 +++ examples/azure/package.json | 47 ++ examples/azure/settings/bot.ts | 282 ++++++++ examples/azure/settings/doc_sample/andy.txt | 3 + examples/azure/settings/helpers/auth.ts | 58 ++ examples/azure/settings/helpers/index.ts | 67 ++ examples/azure/settings/helpers/llm-stream.ts | 41 ++ examples/azure/settings/helpers/prompts.ts | 15 + examples/azure/settings/helpers/storage.ts | 150 ++++ examples/azure/tsconfig.json | 25 + examples/azure/util/cli.ts | 145 ++++ examples/azure/util/index.ts | 63 ++ examples/azure/util/launch.ts | 20 + examples/azure/util/types.ts | 13 + examples/azure/util/websockets.ts | 659 ++++++++++++++++++ examples/standard-server/README.md | 4 +- examples/standard-server/src/index.ts | 2 +- package.json | 2 +- src/index.ts | 3 + src/speedybot.ts | 18 +- 32 files changed, 2288 insertions(+), 109 deletions(-) create mode 100644 docs/examples/azure/README.md create mode 100644 examples/azure/.env.example create mode 100644 examples/azure/README.md create mode 100644 examples/azure/package.json create mode 100644 examples/azure/settings/bot.ts create mode 100644 examples/azure/settings/doc_sample/andy.txt create mode 100644 examples/azure/settings/helpers/auth.ts create mode 100644 examples/azure/settings/helpers/index.ts create mode 100644 examples/azure/settings/helpers/llm-stream.ts create mode 100644 examples/azure/settings/helpers/prompts.ts create mode 100644 examples/azure/settings/helpers/storage.ts create mode 100644 examples/azure/tsconfig.json create mode 100644 examples/azure/util/cli.ts create mode 100644 examples/azure/util/index.ts create mode 100644 examples/azure/util/launch.ts create mode 100644 examples/azure/util/types.ts create mode 100644 examples/azure/util/websockets.ts diff --git a/docs/.vitepress/components/monaco.vue b/docs/.vitepress/components/monaco.vue index bf6d7b0..4121052 100644 --- a/docs/.vitepress/components/monaco.vue +++ b/docs/.vitepress/components/monaco.vue @@ -82,7 +82,17 @@ onMounted(() => { type HeaderConfig = { iconURL?: string; - backgroundColor?: ColorChoices; + backgroundColor?: "Default" + | "Dark" + | "Light" + | "Accent" + | "Good" + | "Warning" + | "Attention" + | "blue" + | "red" + | "green" + | "yellow"; rtl?: boolean; iconSize?: SizeChoices; iconAlignment?: AlignmentChoices; @@ -90,7 +100,17 @@ type HeaderConfig = { iconRound?: boolean; textSize?: SizeChoices; textAlign?: AlignmentChoices; - textColor?: ColorChoices; + textColor?: "Default" + | "Dark" + | "Light" + | "Accent" + | "Good" + | "Warning" + | "Attention" + | "blue" + | "red" + | "green" + | "yellow"; }; type SizeChoices = | "Small" @@ -105,7 +125,17 @@ type SizeChoices = addBlock( content: string | CardBuilder, config: { - backgroundColor?: ColorChoices; + backgroundColor?: "Default" + | "Dark" + | "Light" + | "Accent" + | "Good" + | "Warning" + | "Attention" + | "blue" + | "red" + | "green" + | "yellow"; vertAlign?: VAlignChoices; } = {} ): CardBuilder; @@ -126,8 +156,28 @@ type SizeChoices = config: { size?: SizeChoices; align?: AlignmentChoices; - color?: ColorChoices; - backgroundColor?: ColorChoices; + color?: "Default" + | "Dark" + | "Light" + | "Accent" + | "Good" + | "Warning" + | "Attention" + | "blue" + | "red" + | "green" + | "yellow"; + backgroundColor?: "Default" + | "Dark" + | "Light" + | "Accent" + | "Good" + | "Warning" + | "Attention" + | "blue" + | "red" + | "green" + | "yellow"; vertAlign?: VAlignChoices; } = {}): CardBuilder; addChips( diff --git a/docs/.vitepress/components/token_handler.vue b/docs/.vitepress/components/token_handler.vue index 5899ca6..c87b527 100644 --- a/docs/.vitepress/components/token_handler.vue +++ b/docs/.vitepress/components/token_handler.vue @@ -104,10 +104,13 @@ const store = useCustomStore(); const label = ref("โญ๏ธ"); const emit = defineEmits(); const checkToken = async (tokenCandidate: string) => { + if (!tokenCandidate) store.invalidateToken(); + if (tokenCandidate.length > 5) { await store.validateToken(tokenCandidate); if (store.state.tokenValid) { emit("tokenValidated", { valid: true }); + } else { } } }; diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 580f028..24cfa1c 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -36,6 +36,11 @@ export default defineConfig({ text: "๐Ÿ”ฅ Deploy to Worker ", link: "/examples/worker/README", }, + + { + text: "๐Ÿ”— Bot + Azure LLM", + link: "/examples/azure/README", + }, { text: "๐Ÿฆ– Deploy to Deno", link: "/examples/deno/README", @@ -77,6 +82,10 @@ export default defineConfig({ { text: `v${version}`, items: [ + { + text: "NPM Package", + link: "https://www.npmjs.com/package/speedybot", + }, { text: "Release Notes", link: "https://github.com/valgaze/speedybot/releases", @@ -119,6 +128,10 @@ export default defineConfig({ text: "๐Ÿฆ– Deploy to Deno", link: "/examples/deno/README", }, + { + text: "๐Ÿ”— Bot + Azure LLM", + link: "/examples/azure/README", + }, { text: "๐ŸŒ Deploy to Server ", link: "/examples/standard-server/README", diff --git a/docs/.vitepress/util/store.ts b/docs/.vitepress/util/store.ts index 4ec514c..b716779 100644 --- a/docs/.vitepress/util/store.ts +++ b/docs/.vitepress/util/store.ts @@ -69,6 +69,10 @@ function addWebhook(webhook: Webhook) { store.webhooks.push(webhook); } +async function invalidateToken() { + store.tokenValid = false; +} + async function validateToken(tokenCandidate: string): Promise { const loading = ElLoading.service({ lock: true, @@ -141,6 +145,7 @@ export const storeHelper = { setToken, addWebhook, validateToken, + invalidateToken, cycle, setSearchlevel, }; diff --git a/docs/api-docs/README.md b/docs/api-docs/README.md index 2d28099..bd4cf1c 100644 --- a/docs/api-docs/README.md +++ b/docs/api-docs/README.md @@ -31,7 +31,7 @@ npm install speedybot ## SpeedyBot basics -You can get a bot up and running fast by grabbing one of the batteries-included samples at **[https://speedybot.js.org/examples](https://speedybot.js.org/examples/)** and see how SpeedyBot has you covered for crafting bots that can do it all-- **[securely integrate w/ LLMs + content management systems](https://speedybot.js.org/examples/voiceflow/README)**, **[process file-uploads](https://speedybot.js.org/patterns.md#handle-file-uploads)**, **[segment content based on user data + behavior](https://speedybot.js.org/patterns.md#restrict-emails)**, create + manage **[SpeedyCards](https://speedybot.js.org/speedycard)**, **[ask for a user's location in a privacy-respecting way](https://speedybot.js.org/examples/location/README)** and lots more. +You can get a bot up and running fast by grabbing one of the batteries-included samples at **[https://speedybot.js.org/examples](https://speedybot.js.org/examples/)** and see how SpeedyBot has you covered for crafting bots that can do it all-- **[securely integrate w/ LLMs + content management systems](https://speedybot.js.org/examples/voiceflow/README)**, **[process file-uploads](https://speedybot.js.org/patterns#handle-file-uploads)**, **[segment content based on user data + behavior](https://speedybot.js.org/patterns#restrict-emails)**, create + manage **[SpeedyCards](https://speedybot.js.org/speedycard)**, **[ask for a user's location in a privacy-respecting way](https://speedybot.js.org/examples/location/README)** and lots more. ## The basics @@ -100,7 +100,7 @@ Bot.addStep(async ($) => { ## Garage -SpeedyBot's docs are special-- they're interactive and you can do things with them. From the Patterns docs you can grab **[code snippets](https://speedybot.js.org/patterns)** and throw them right into your bot. Inside the visaul **[SpeedyBot Garage](https://speedybot.js.org/garage)** experience you can register webhooks and design + preview + send **[SpeedyCards](https://speedybot.js.org/speedycard)** +SpeedyBot's docs are special-- they're interactive and you can do things with them. From the Patterns docs you can grab **[code snippets](https://speedybot.js.org/patterns)** and throw them right into your bot. Inside the visual **[SpeedyBot Garage](https://speedybot.js.org/garage)** experience you can register webhooks and design + preview + send **[SpeedyCards](https://speedybot.js.org/speedycard)** @@ -121,3 +121,45 @@ SpeedyBot makes it speedy & easy to build serverless bots for the LLM era. See t ## ๐Ÿ Speedybot-Python If you want to build bots with Python rather than Typescript, you can also check out [๐ŸSpeedybot-Python๐Ÿ](https://pypi.org/project/speedybot) + +## CLI + +It's SpeedyBot all the way down-- the **[SpeedyBot Documentation](https://speedybot.js.org)** is powered by SpeedyBot but SpeedyBot also has a fast and powerful CLI. + +- Can run as `npm init speedybot@latest` or `npx -y speedybot` + +- Add `--help` flag to end of commands (ex. `npx -y speedybot setup --help`) + +## Setup + +Download, scaffold, setup, and even boot SpeedyBot projects locally + +``` +npm init speedybot@latest setup -- --help +npx -y speedybot@latest setup --help +npx -y speedybot@latest setup +npx -y speedybot@^2.0.0 setup --project default --boot --install +npx -y speedybot@^2.0.0 setup --project voiceflow-kb -e BOT_TOKEN -e VOICEFLOW_API_KEY --install --boot +``` + +## Token + +Inspect a WebEx token, see if its valid and see if any associated agents + +``` +npm init speedybot@latest token -- --help +npx -y speedybot@latest token --help +``` + +## Webhook + +Manage webhooks-- Create, List, and Destroy webhooks associated with a token + +``` +npm init speedybot@latest webhook -- --help +npx -y speedybot@latest webhook --help +npx -y speedybot@latest webhook list +npx -y speedybot@latest webhook create -w https://www.myinfra.com -t tokenvalue -s secretvalue + +npx -y speedybot@latest webhook remove +``` diff --git a/docs/api-docs/classes/SpeedyBot.md b/docs/api-docs/classes/SpeedyBot.md index bc87e7a..18b4cb1 100644 --- a/docs/api-docs/classes/SpeedyBot.md +++ b/docs/api-docs/classes/SpeedyBot.md @@ -84,7 +84,7 @@ #### Defined in -[speedybot.ts:55](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L55) +[speedybot.ts:55](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L55) ## Methods @@ -107,7 +107,7 @@ Create firehose and attachmentActions webhooks #### Defined in -[speedybot.ts:934](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L934) +[speedybot.ts:932](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L932) ___ @@ -128,7 +128,7 @@ ___ #### Defined in -[speedybot.ts:69](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L69) +[speedybot.ts:69](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L69) ___ @@ -148,7 +148,7 @@ ___ #### Defined in -[speedybot.ts:73](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L73) +[speedybot.ts:73](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L73) ___ @@ -191,7 +191,7 @@ Bot.addStep(async ($) => { #### Defined in -[speedybot.ts:154](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L154) +[speedybot.ts:154](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L154) ___ @@ -217,7 +217,7 @@ ___ #### Defined in -[speedybot.ts:158](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L158) +[speedybot.ts:158](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L158) ___ @@ -239,7 +239,7 @@ ___ #### Defined in -[speedybot.ts:1427](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L1427) +[speedybot.ts:1425](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L1425) ___ @@ -259,7 +259,7 @@ ___ #### Defined in -[speedybot.ts:247](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L247) +[speedybot.ts:247](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L247) ___ @@ -307,7 +307,7 @@ CultureBot.contains(["hi", "hey"], #### Defined in -[speedybot.ts:712](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L712) +[speedybot.ts:710](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L710) ___ @@ -327,7 +327,7 @@ ___ #### Defined in -[speedybot.ts:1026](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L1026) +[speedybot.ts:1024](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L1024) ___ @@ -351,7 +351,7 @@ If a match is found, the provided middleware function is executed. #### Defined in -[speedybot.ts:223](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L223) +[speedybot.ts:223](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L223) ___ @@ -388,7 +388,7 @@ The converted object. #### Defined in -[speedybot.ts:1419](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L1419) +[speedybot.ts:1417](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L1417) ___ @@ -413,7 +413,7 @@ ___ #### Defined in -[speedybot.ts:1010](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L1010) +[speedybot.ts:1008](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L1008) ___ @@ -433,7 +433,7 @@ ___ #### Defined in -[speedybot.ts:859](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L859) +[speedybot.ts:857](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L857) ___ @@ -453,7 +453,7 @@ ___ #### Defined in -[speedybot.ts:872](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L872) +[speedybot.ts:870](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L870) ___ @@ -473,7 +473,7 @@ ___ #### Defined in -[speedybot.ts:844](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L844) +[speedybot.ts:842](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L842) ___ @@ -497,7 +497,7 @@ Edit a message #### Defined in -[speedybot.ts:794](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L794) +[speedybot.ts:792](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L792) ___ @@ -518,7 +518,7 @@ ___ #### Defined in -[speedybot.ts:777](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L777) +[speedybot.ts:775](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L775) ___ @@ -542,7 +542,7 @@ If there is a match, the provided middleware function is executed. #### Defined in -[speedybot.ts:205](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L205) +[speedybot.ts:205](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L205) ___ @@ -569,7 +569,7 @@ ___ #### Defined in -[speedybot.ts:1300](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L1300) +[speedybot.ts:1298](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L1298) ___ @@ -585,7 +585,7 @@ Return abbreviated array of webhook data #### Defined in -[speedybot.ts:907](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L907) +[speedybot.ts:905](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L905) ___ @@ -608,7 +608,7 @@ Utility to traverse Link headers for pagination, built-in back-off #### Defined in -[speedybot.ts:1395](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L1395) +[speedybot.ts:1393](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L1393) ___ @@ -622,7 +622,7 @@ ___ #### Defined in -[speedybot.ts:1140](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L1140) +[speedybot.ts:1138](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L1138) ___ @@ -644,7 +644,7 @@ ___ #### Defined in -[speedybot.ts:1244](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L1244) +[speedybot.ts:1242](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L1242) ___ @@ -664,7 +664,7 @@ ___ #### Defined in -[speedybot.ts:1077](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L1077) +[speedybot.ts:1075](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L1075) ___ @@ -684,7 +684,7 @@ ___ #### Defined in -[speedybot.ts:943](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L943) +[speedybot.ts:941](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L941) ___ @@ -704,7 +704,7 @@ ___ #### Defined in -[speedybot.ts:1091](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L1091) +[speedybot.ts:1089](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L1089) ___ @@ -724,7 +724,7 @@ ___ #### Defined in -[speedybot.ts:77](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L77) +[speedybot.ts:77](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L77) ___ @@ -744,7 +744,7 @@ ___ #### Defined in -[speedybot.ts:1032](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L1032) +[speedybot.ts:1030](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L1030) ___ @@ -758,7 +758,7 @@ ___ #### Defined in -[speedybot.ts:107](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L107) +[speedybot.ts:107](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L107) ___ @@ -774,7 +774,7 @@ Return array of full webhook data #### Defined in -[speedybot.ts:889](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L889) +[speedybot.ts:887](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L887) ___ @@ -794,7 +794,7 @@ ___ #### Defined in -[speedybot.ts:115](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L115) +[speedybot.ts:115](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L115) ___ @@ -819,7 +819,7 @@ For use w/ vision + text prompting systems #### Defined in -[speedybot.ts:185](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L185) +[speedybot.ts:185](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L185) ___ @@ -839,7 +839,7 @@ ___ #### Defined in -[speedybot.ts:241](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L241) +[speedybot.ts:241](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L241) ___ @@ -865,7 +865,7 @@ ___ #### Defined in -[speedybot.ts:122](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L122) +[speedybot.ts:122](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L122) โ–ธ **pickRandom**<`P`\>(`min`, `max`): `number` @@ -888,7 +888,7 @@ ___ #### Defined in -[speedybot.ts:123](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L123) +[speedybot.ts:123](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L123) โ–ธ **pickRandom**<`P`\>(`listOrMin`, `max?`): `number` \| `P` @@ -911,7 +911,7 @@ ___ #### Defined in -[speedybot.ts:124](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L124) +[speedybot.ts:124](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L124) ___ @@ -927,7 +927,7 @@ Generate a random string of 11 characters (letters + numbers) #### Defined in -[speedybot.ts:1147](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L1147) +[speedybot.ts:1145](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L1145) ___ @@ -948,19 +948,19 @@ ___ #### Defined in -[speedybot.ts:162](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L162) +[speedybot.ts:162](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L162) ___ ### replyTo -โ–ธ **replyTo**(`param1`, `param2`, `param3?`): `Promise`<`MessageResponse`\> +โ–ธ **replyTo**(`roomIdParam`, `param2`, `param3?`): `Promise`<`MessageResponse`\> #### Parameters | Name | Type | | :------ | :------ | -| `param1` | `string` \| `MessageResponse` | +| `roomIdParam` | `string` \| `MessageResponse` | | `param2` | `undefined` \| `string` | | `param3?` | `string` | @@ -970,7 +970,7 @@ ___ #### Defined in -[speedybot.ts:814](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L814) +[speedybot.ts:812](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L812) ___ @@ -998,7 +998,7 @@ import #### Defined in -[speedybot.ts:262](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L262) +[speedybot.ts:262](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L262) ___ @@ -1035,7 +1035,7 @@ $bot.sendFileTo('target@email.com', myData, 'json') #### Defined in -[speedybot.ts:1188](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L1188) +[speedybot.ts:1186](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L1186) ___ @@ -1070,7 +1070,7 @@ Bot.sendTo({toPersonId: 'xxxyyyzzz', '**here is a message**') #### Defined in -[speedybot.ts:626](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L626) +[speedybot.ts:624](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L624) ___ @@ -1090,7 +1090,7 @@ ___ #### Defined in -[speedybot.ts:111](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L111) +[speedybot.ts:111](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L111) ___ @@ -1112,7 +1112,7 @@ Sets the token to transact with APIs (needed to send messages, receive webhooks, #### Defined in -[speedybot.ts:102](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L102) +[speedybot.ts:102](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L102) ___ @@ -1135,7 +1135,7 @@ ___ #### Defined in -[speedybot.ts:766](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L766) +[speedybot.ts:764](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L764) ___ @@ -1156,7 +1156,7 @@ ___ #### Defined in -[speedybot.ts:784](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L784) +[speedybot.ts:782](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L782) ___ @@ -1176,7 +1176,7 @@ ___ #### Defined in -[speedybot.ts:669](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L669) +[speedybot.ts:667](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L667) ___ @@ -1190,4 +1190,4 @@ ___ #### Defined in -[speedybot.ts:1046](https://github.com/valgaze/speedybot/blob/4280279/src/speedybot.ts#L1046) +[speedybot.ts:1044](https://github.com/valgaze/speedybot/blob/e6d093d/src/speedybot.ts#L1044) diff --git a/docs/api-docs/classes/SpeedyCard.md b/docs/api-docs/classes/SpeedyCard.md index fb9d0d3..1295a7a 100644 --- a/docs/api-docs/classes/SpeedyCard.md +++ b/docs/api-docs/classes/SpeedyCard.md @@ -59,7 +59,7 @@ You can add "pickers" for date, time, select (and multi-select) #### Defined in -[cards.ts:281](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L281) +[cards.ts:281](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L281) ## Properties @@ -75,7 +75,7 @@ You can add "pickers" for date, time, select (and multi-select) #### Defined in -[cards.ts:217](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L217) +[cards.ts:217](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L217) ## Methods @@ -95,7 +95,7 @@ You can add "pickers" for date, time, select (and multi-select) #### Defined in -[cards.ts:748](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L748) +[cards.ts:748](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L748) ___ @@ -119,7 +119,7 @@ ___ #### Defined in -[cards.ts:521](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L521) +[cards.ts:521](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L521) ___ @@ -141,7 +141,7 @@ ___ #### Defined in -[cards.ts:763](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L763) +[cards.ts:763](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L763) ___ @@ -162,7 +162,7 @@ ___ #### Defined in -[cards.ts:315](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L315) +[cards.ts:315](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L315) ___ @@ -183,7 +183,7 @@ ___ #### Defined in -[cards.ts:322](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L322) +[cards.ts:322](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L322) ___ @@ -203,7 +203,7 @@ ___ #### Defined in -[cards.ts:755](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L755) +[cards.ts:755](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L755) ___ @@ -224,7 +224,7 @@ ___ #### Defined in -[cards.ts:455](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L455) +[cards.ts:455](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L455) ___ @@ -248,7 +248,7 @@ ___ #### Defined in -[cards.ts:358](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L358) +[cards.ts:358](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L358) ___ @@ -269,7 +269,7 @@ ___ #### Defined in -[cards.ts:388](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L388) +[cards.ts:388](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L388) ___ @@ -290,7 +290,7 @@ ___ #### Defined in -[cards.ts:392](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L392) +[cards.ts:392](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L392) ___ @@ -311,7 +311,7 @@ ___ #### Defined in -[cards.ts:612](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L612) +[cards.ts:612](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L612) ___ @@ -332,7 +332,7 @@ ___ #### Defined in -[cards.ts:660](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L660) +[cards.ts:660](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L660) ___ @@ -353,7 +353,7 @@ ___ #### Defined in -[cards.ts:575](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L575) +[cards.ts:575](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L575) ___ @@ -374,7 +374,7 @@ ___ #### Defined in -[cards.ts:672](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L672) +[cards.ts:672](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L672) ___ @@ -395,7 +395,7 @@ ___ #### Defined in -[cards.ts:602](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L602) +[cards.ts:602](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L602) ___ @@ -416,7 +416,7 @@ ___ #### Defined in -[cards.ts:561](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L561) +[cards.ts:561](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L561) ___ @@ -436,7 +436,7 @@ ___ #### Defined in -[cards.ts:288](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L288) +[cards.ts:288](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L288) ___ @@ -457,7 +457,7 @@ ___ #### Defined in -[cards.ts:293](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L293) +[cards.ts:293](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L293) ___ @@ -484,7 +484,7 @@ ___ #### Defined in -[cards.ts:406](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L406) +[cards.ts:406](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L406) ___ @@ -505,7 +505,7 @@ ___ #### Defined in -[cards.ts:685](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L685) +[cards.ts:685](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L685) ___ @@ -526,7 +526,7 @@ ___ #### Defined in -[cards.ts:698](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L698) +[cards.ts:698](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L698) ___ @@ -546,7 +546,7 @@ ___ #### Defined in -[cards.ts:283](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L283) +[cards.ts:283](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L283) ___ @@ -566,7 +566,7 @@ ___ #### Defined in -[cards.ts:723](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L723) +[cards.ts:723](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L723) ___ @@ -580,7 +580,7 @@ ___ #### Defined in -[cards.ts:817](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L817) +[cards.ts:817](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L817) ___ @@ -594,7 +594,7 @@ ___ #### Defined in -[cards.ts:263](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L263) +[cards.ts:263](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L263) ___ @@ -614,7 +614,7 @@ ___ #### Defined in -[cards.ts:712](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L712) +[cards.ts:712](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L712) ___ @@ -634,7 +634,7 @@ ___ #### Defined in -[cards.ts:717](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L717) +[cards.ts:717](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L717) ___ @@ -655,4 +655,4 @@ ___ #### Defined in -[cards.ts:779](https://github.com/valgaze/speedybot/blob/4280279/src/cards.ts#L779) +[cards.ts:779](https://github.com/valgaze/speedybot/blob/e6d093d/src/cards.ts#L779) diff --git a/docs/api-docs/modules.md b/docs/api-docs/modules.md index eab1c80..534ba5f 100644 --- a/docs/api-docs/modules.md +++ b/docs/api-docs/modules.md @@ -27,4 +27,4 @@ #### Defined in -[types.ts:122](https://github.com/valgaze/speedybot/blob/4280279/src/types.ts#L122) +[types.ts:122](https://github.com/valgaze/speedybot/blob/e6d093d/src/types.ts#L122) diff --git a/docs/examples/azure/README.md b/docs/examples/azure/README.md new file mode 100644 index 0000000..f85198c --- /dev/null +++ b/docs/examples/azure/README.md @@ -0,0 +1,342 @@ +# ๐Ÿ”— Integrate with Azure + +This guide will show you how to set up SpeedyBot on your local machine and connect it to a large language model managed by Azure. SpeedyBot streamlines the process of designing, deploying, and securing sophisticated conversation systems, tailored for enterprises and large teams with complex needs. + +## Step 0: Get your Azure details + +To authenticate with Azure, you'll need to obtain some key data. Your organization might be able to provide this information. + +```sh +BOT_TOKEN=__REPLACE__ME__ +OAUTH_ENDPOINT=https://yourDomain.com/oauth2/default/v1/token +CLIENT_ID=aabbccddeeffgghhiijjkk +CLIENT_SECRET=abcd1234567890987654321 + +BASE_URL_LLM=https://{your-resource-name}.openai.azure.com/openai +MODEL=gpt-35-turbo +APP_KEY=app-name-here +API_VERSION=2023-12-01-preview +``` + +Further reading: + +- Setup: https://learn.microsoft.com/en-us/azure/ai-services/openai/quickstart?tabs=command-line&pivots=rest-api + +- https://learn.microsoft.com/en-us/azure/ai-services/openai/reference + +- https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/content-filter?tabs=warning%2Cpython-new + +- https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/chatgpt?tabs=python-new + +## Step I: Grab Your Access Token + +- **Sign Up**: You'll need a WebEx account to build botsโ€” if you don't have one one, sign up for a new account here: **[https://signup.webex.com/sign-up](https://signup.webex.com/sign-up)** + +- **Create a Bot**: Once you have an account, create a new bot and copy its access token from here: **[https://developer.webex.com/my-apps/new/bot](https://developer.webex.com/my-apps/new/bot)** + +The flow to get a token will look roughly like this: + + + +### Validate Token + +Once you've got your token, pop it into the box below to validate it & review your bot's details + + + +::: details Is this safe?? + +Your bot token is a **highly** sensitive credential and should be protected with encryption and proper secrets management. + +SpeedyBot does **NOT** log/persist or do anything (except what you tell it to do) with your bot token. + +**REMEMBER:** If your agent's access token is ever compromised/exposed, you can always invalidate it + get a new one by tapping "Regenerate Access Token" under your agent's settings page + + + +::: + +## Step II: Get your System Ready + +To keep things simple at the start, you'll run the bot from your own machine (meaning when your computer is off, your bot is "off" too). Later, if needed, you can deploy your bot to virtually any standard server or scalable serverless cloud infrastructure you prefer + + + + +
+ +### Getting Started with Bun on Your Computer + +Bun is a tool that helps make working with JavaScript faster and easier. It's like a special tool that helps you run JavaScript code really quickly. You can find helpful guides and explanations about Bun [here](https://bun.sh/docs). + +Setting up Bun is simple. It ships as a single executable that you can easily install on your computer. If you're not interested in using Bun, that's okay too. SpeedyBot works with many runtimes and infrastructure-- see the NodeJS instructions by tapping the toggle above + +Enter the commands below in your terminal to get up and running with Bun: + +::: code-group + +```sh-vue [Linux/Mac] +curl -fsSL https://bun.sh/install | bash +``` + +```sh-vue [Windows (must use powershell)] +powershell -c "irm bun.sh/install.ps1 | iex" +``` + +Verify that Bun is installed correctly and available from your terminal-- if you can run `bun --version` in your terminal and you see a version number you're good to go! + +::: + +::: details ๐Ÿ’ก Tips for Windows Users + Bun Questions + +**[Bun](https://bun.sh)** is an experimental high-performance JavaScript runtime and toolkit that combines a package manager, bundler, and task runner into one tool. + +Note: Install scripts are available for inspection below + +- For Mac: [install.sh](https://github.com/oven-sh/bun/blob/main/src/cli/install.sh) +- For Windows: [install.ps1](https://github.com/oven-sh/bun/blob/main/src/cli/install.ps1) + +SpeedyBot can run on any Javascript/Typescript runtime or infrastructue-- Bun just happens to be a speedy and easy way to get up and running + +### Note for Mac Users + +If you're running an older version of Mac OS and you encounter errors when installing Bun try **[upgrading your operating system](https://support.apple.com/en-us/108382)** + +### Note for Windows Users + +On PC if you're having trouble getting `Bun` working on Windows/Powershell try the following method instead: + +Step 1: Download and run Git Bash https://git-scm.com/download/win + +**Note:** Step-by-step instructions available here: https://www.git-tower.com/blog/git-bash/ + +Step 2: Run the following command inside Git Bash to setup Bun: + +``` +curl -fsSL https://bun.sh/install | bash +``` + +Step 3: Completely close out from Git Bash + +Step 4: Re-open Git Bash again and try **[Step III](#step-3)** + +::: + +## Step III: Boot it up! {#step-3} + +Copy the command below into your terminal to turn on your bot + +::: code-group + +```sh-vue [๐Ÿš€ Bun (FAST!!)] +bunx --bun create-speedybot@2.0.9 setup -e OAUTH_ENDPOINT -e BASE_URL_LLM -e MODEL -e CLIENT_ID -e CLIENT_SECRET -e APP_KEY -e API_VERSION --project azure --boot --bun --install {{ store.state.tokenValid ? `--token ${store.state.token}` : '' }} +``` + +```sh-vue [๐Ÿ‘น Experienced] +git clone --depth 1 https://github.com/valgaze/speedybot +cd speedybot +cd examples/azure +bun install +bun util/cli.ts setup {{ store.state.tokenValid ? store.state.token : '__ACCESS__TOKEN__HERE__' }} +cp .env.example .env # Fill out .env with real values +bun --watch util/launch.ts +``` + +::: + +
+ +
+ +### Setup Node on Your Computer + +Node is a popular tool for running Typescript/JavaScript on web servers. Today, you'll use it to activate your botโ€”it will handle all the details behind the scenes. + +You'll need to install NodeJS on your machine to turn on your bot. There are many ways to do that, but two easy ways: + +**Option 1** Download + install Node from the official site: **[https://nodejs.org/en/download](https://nodejs.org/en/download)** + +or + +**Option 2** Download with **[Volta](https://docs.volta.sh/guide/)** in the terminal + +```sh +curl https://get.volta.sh | bash + +volta install node +``` + +Note: If you're having trouble getting setup on Windows see the **[Windows Quickstart](./../../windows.md)** + +Whatever option you choose, verify that Node.js is installed, enter `node -v` in your terminal-- if you see a version number you're good to go! + +## Step III: Boot it up! + +Once Node is on your machine, copy the command below into your terminal to turn on your bot + +::: code-group + +```sh-vue [๐Ÿฅบ npx (recommended)] +npx -y speedybot@2.0.9 setup -e OAUTH_ENDPOINT -e BASE_URL_LLM -e MODEL -e CLIENT_ID -e CLIENT_SECRET -e APP_KEY -e API_VERSION --project azure --boot --install {{ store.state.tokenValid ? `--token ${store.state.token}` : '' }} +``` + +```sh-vue [๐Ÿ‘น Manual Steps (For experienced)] +git clone --depth 1 https://github.com/valgaze/speedybot +cd speedybot +cd examples/speedybot-starter +npm i +npm run bot:setup {{ store.state.tokenValid ? store.state.token : '__ACCESS__TOKEN__HERE__' }} +cp .env.example .env # Fill out .env with real values +npm run dev +``` + +::: + +
+ +You can turn off your bot by holding down **CTRL-C** on your keyboard or exiting the terminal. To turn your bot back "on", open your terminal to your project directory and enter `bun run dev` + +Note: when you press **CTRL-C** the terminal will display the location of your bot + + + +## Step IV: Customize your agent + +Once your bot is loaded, you can customize the bot.ts file to choose which experience you want to deliver to your users. If you've never edited a bot before see **[here for the basics](https://speedybot.js.org/patterns#the-basics)** + +Under the first step in `bot.ts` you need to pick one of three LLM strategies to configure your bot's behavior with Azure (ie simple, threaded conversation threads, or streaming) hit save and the bot will reload: + + + +Ex. If you selected "streamExample" response tokens will "stream" in to the chat client as they arrive (rather than forcing your users to wait for the full completion): + + + +## Things to try + +- Additionally, you can take a look at the **[system prompts](https://github.com/valgaze/speedybot/blob/v2/examples/azure/settings/helpers/prompts.ts)** and tune this starter sample to suit your requirements + +- Work with file uploads to see how to extract file data and get meta-data (file-name, file-type, size, etc), try uploading. + +Upload a file and use the example code to extract file data like name, type, and size. Test with the **["andy.txt" file](https://github.com/valgaze/speedybot/blob/v2/examples/azure/settings/doc_sample/andy.txt)** to see how you can inject file content into a conversation context + +Note: To customize your bot's behavior or its responses, you'll need an editor-- one popular choice is Visual Studio Code. + +For installation details, visit **[https://code.visualstudio.com/download](https://code.visualstudio.com/download)** + + + +It works great with SpeedyBot and even provides helpful hints as you build ex. + + + +When you're ready to deploy it to a server, serverless function or virtually any infrastructure/device, **[check out the examples](./../../examples.md)** + + + + diff --git a/docs/examples/standard-server/README.md b/docs/examples/standard-server/README.md index e0359ab..e179f6b 100644 --- a/docs/examples/standard-server/README.md +++ b/docs/examples/standard-server/README.md @@ -62,10 +62,12 @@ curl -X POST -H "Content-Type: application/json" -d '{"id": 1234567890987654321} padding: 10px; "/> -## 6) Supply your Webhook "secret" to your Worker +## 6) Supply your Webhook "secret" to your server Even though it's "optional", it's a really, really good idea to set a Webhook Secret too so you can make sure incoming requests are the real deal. For more detail, see **[https://speedybot.js.org/webhooks#securing-webhooks](https://speedybot.js.org/webhooks#securing-webhooks)** +To add a secret to an existing webhook, delete the current webhook and create a new one with the secret. + ## 7) Take it for a spin Bun is a tool that helps make working with JavaScript faster and easier. It's like a special tool that helps you run JavaScript code really quickly. You can find helpful guides and explanations about Bun [here](https://bun.sh/docs). Setting up Bun is simple. It ships as a single executable that you can easily install on your computer. If you're not interested in using Bun, that's okay too. SpeedyBot works with many runtimes and infrastructure-- see the NodeJS instructions by tapping the toggle above -Enter the commands below in your terminal to get up and running with Bun + +Enter the commands below in your terminal to get up and running with Bun: ::: code-group @@ -139,6 +140,10 @@ Note: Install scripts are available for inspection below SpeedyBot can run on any Javascript/Typescript runtime or infrastructue-- Bun just happens to be a speedy and easy way to get up and running +### Note for Mac Users + +If you're running an older version of Mac OS and you encounter errors when installing Bun try **[upgrading your operating system](https://support.apple.com/en-us/108382)** + ### Note for Windows Users On PC if you're having trouble getting `Bun` working on Windows/Powershell try the following method instead: @@ -286,7 +291,7 @@ Whether you're just starting out on your conversation design journey or a season When you're ready to deploy it to a server, serverless function or virtually any infrastructure/device, **[check out the examples](./examples.md)** diff --git a/examples/azure/.env.example b/examples/azure/.env.example new file mode 100644 index 0000000..4e971aa --- /dev/null +++ b/examples/azure/.env.example @@ -0,0 +1,13 @@ +BOT_TOKEN=__REPLACE__ME__ +OAUTH_ENDPOINT=https://yourDomain.com/oauth2/default/v1/token +CLIENT_ID=aabbccddeeffgghhiijjkk +CLIENT_SECRET=abcd1234567890987654321 + +BASE_URL_LLM=https://{your-resource-name}.openai.azure.com/openai +MODEL=gpt-35-turbo +APP_KEY=app-name-here +API_VERSION=2023-12-01-preview + +# Save this file as .env with: $ cp .env.example .env +# Replace BOT_TOKEN with your Bot Access token +# Replace other items specific to your implementation/architecture \ No newline at end of file diff --git a/examples/azure/README.md b/examples/azure/README.md new file mode 100644 index 0000000..b611588 --- /dev/null +++ b/examples/azure/README.md @@ -0,0 +1,94 @@ +## Speedybot Azure + +This sample connects to Azure OpenAI services and provides pluck'able reference implementations to support threaded conversation histories/contexts, streaming responses, and other features. + +## 1) Clone repo & install dependencies + +``` +git clone https://github.com/valgaze/speedybot +cd examples/speedybot-azure +npm install +``` + +## 2) Set your bot access token + +- Make a new bot and note its access token from here: **[https://developer.webex.com/my-apps/new/bot](https://developer.webex.com/my-apps/new/bot)** + +You can set your `BOT_TOKEN` by running this script in the project directory: + +`npm run bot:setup ` + +
Set token by hand +Copy the file **[.env.example](.env.example)** as `.env` in the root of your project and save your access token under the `BOT_TOKEN` field, ex + +``` +BOT_TOKEN=__REPLACE__ME__ +``` + +
+ +## 2A) Gather your details + +You need quite a bit of sensitive data to get up and running-- you'll need a `.env` file in the root of this project with the following information (see **[.env.example](./.env.example)** for reference): + +``` +BOT_TOKEN=__REPLACE__ME__ +OAUTH_ENDPOINT=https://yourDomain.com/oauth2/default/v1/token +CLIENT_ID=aabbccddeeffgghhiijjkk +CLIENT_SECRET=abcd1234567890987654321 + +BASE_URL_LLM=https://{your-resource-name}.openai.azure.com/openai +MODEL=gpt-35-turbo +APP_KEY=app-name-here +API_VERSION=2023-12-01-preview +``` + +## 3) Boot it up! + +- Start up your agent + +Note: By default your agent will communicate using websockets, so you won't need to worry about details like deployment or webhooks. Later down the line if you need to deploy your agent on a traditional server or ephemeral/serverless function infrastructure **[see the examples](https://speedybot.js.org/examples)** + +``` +npm run bot:dev +``` + +## NPM Run Scripts + +All you'll probably need are `npm run bot:dev` + maybe `npm run bot:reset` + +| Script | Description | +| ----------------------- | ---------------------------------------------- | +| `npm run bot:on` | Launches the SpeedyBot | +| `npm run serve` | Alias for npm run bot:dev | +| `npm run dev` | Alias for npm run bot:dev | +| `npm run bot:debug` | Displays environment information for debugging | +| `npm run bot:dev` | Launches the bot in development mode | +| `npm run bot:reset` | Resets the bot's configuration | +| `npm run bot:setup` | Sets up the bot for the first time | +| `npm run bot:token` | Alias for npm run bot:setup | +| `npm run bot:help` | Displays help information for the bot | +| `npm run help` | Alias for npm run bot:help | +| `npm run bot:addsecret` | Adds a secret to the bot's configuration | + +
Getting errors? + +If you see an error like `npm: command not found` you probably need to install node or compatible runtime (like **[bun](https://bun.sh)** or **[deno](https://deno.com)**) onto your system. + +There are many ways to do this, but two easy ways: + +Option 1. Download + install Node from the official site: **[https://nodejs.org/en/download](https://nodejs.org/en/download)** + +Option 2. Download with **[Volta](https://docs.volta.sh/guide/)** in the terminal + +```sh +curl https://get.volta.sh | bash + +volta install node +``` + +However you set up your system, make sure to run `node -v` in your terminal to verify node is correctly installed and you can take advantage of its rich ecoysten + +
+ +If you see an error that reads somnething like `Forbidden: User has excessive device registrations` you can run `npm run bot:reset`, wait a few minutes and try again diff --git a/examples/azure/package.json b/examples/azure/package.json new file mode 100644 index 0000000..d93d00f --- /dev/null +++ b/examples/azure/package.json @@ -0,0 +1,47 @@ +{ + "name": "speedybot-starter", + "version": "2.0.0", + "description": "The speedy and easy way to build rich bots + conversation experiences", + "main": "index.ts", + "scripts": { + "dev:bun": "bun --watch util/launch.ts", + "bot:on": "tsx util/launch", + "serve": "npm run bot:dev", + "dev": "npm run bot:dev", + "bot:debug": "npx -y envinfo@7.11.0", + "bot:serve": "npm run bot:dev", + "bot:dev": "tsx watch util/launch", + "bot:reset": "tsx util/cli reset", + "bot:setup": "tsx util/cli setup", + "bot:token": "npm run bot:setup", + "bot:help": "tsx util/cli help", + "help": "npm run bot:help", + "bot:addsecret": "tsx util/cli addsecret" + }, + "keywords": [ + "bot", + "scaffold", + "continerless", + "speedybot", + "serverless" + ], + "author": "valgaze@gmail.com", + "license": "MIT", + "devDependencies": { + "@types/node": "^16.18.3", + "@types/ws": "^8.5.10", + "cross-fetch": "^3.1.5", + "dotenv": "^16.0.3", + "tsx": "^4.7.0", + "typescript": "^3.8.3", + "ws": "^8.17.0" + }, + "dependencies": { + "openai": "^4.52.4", + "speedybot": "^2.0.0", + "sqlite3": "^5.1.7" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/examples/azure/settings/bot.ts b/examples/azure/settings/bot.ts new file mode 100644 index 0000000..5843259 --- /dev/null +++ b/examples/azure/settings/bot.ts @@ -0,0 +1,282 @@ +import { AzureOpenAI } from "openai"; +import { SpeedyBot } from "speedybot"; +import { LLMStream, OAuthHelper, SimpleMemoryStorage } from "./helpers"; +import { SystemPrompt } from "./helpers/prompts"; +import type { CustomAzureRes, LLMSamples, SecretsList } from "./helpers"; + +const StorageRef = new SimpleMemoryStorage(":memory:"); + +const Bot = new SpeedyBot(); +export default Bot; + +Bot.exact("$clear", async ($) => !Boolean(await $.clearScreen())); + +/** + * Setup: Choose an LLM strategy to configure the bot's behavior. + * - 'simpleExample': Sends an input and gets a single response. + * - 'threadsExample': Sends an input and receives multiple responses organized by threads/replies. + * - 'streamExample': Streams tokens as they are received from an LLM system. + */ +Bot.addStep(($) => { + const LLMStrategy: LLMSamples = "simpleExample"; + $.ctx.LLMStrategy = LLMStrategy; + $.ctx.debug = true; + + return $.next; +}); + +/** + * Example 1: "Simple" + * Sends a single-shot message where the LLM responds based on system prompt + user input. + * This sample does NOT maintain conversation memory/context. + */ +Bot.addStep(async ($) => { + if ($.ctx.LLMStrategy === "simpleExample") { + try { + if ($.text) { + const client = await buildAzureClient(Bot); + const appKey = Bot.getSecret("APP_KEY") as string; + const modelName = Bot.getSecret("MODEL") as string; + + const result = await client.chat.completions.create({ + model: modelName, + messages: [ + { role: "system", content: SystemPrompt }, + { role: "user", content: $.text }, + ], + user: JSON.stringify({ appkey: appKey }), + stop: ["<|im_end|>"], + stream: false, + }); + + const responseText = result.choices[0]?.message?.content; + if (responseText) { + await $.reply(responseText); + } + } + } catch (e) { + if (e && typeof e === "object" && "status" in e && e.status === 401) { + await $.send("It appears there is an issue with the system's API key"); + return $.end; + } + await $.send("It appears there was some type of catastrophic error"); + if ($.ctx.debug) { + console.log("Error", e); + await $.send(JSON.stringify(e)); + } + } + } + return $.next; +}); + +/** + * Example 2: "Threads" + * Allows replying to messages where the LLM maintains conversation context and history, within context window limits. + */ +Bot.addStep(async ($) => { + if ($.ctx.LLMStrategy === "threadsExample") { + try { + if ($.text) { + let { parentId } = $.msg; + const isThread = Boolean(parentId); + if (!isThread) { + const text = $.pickRandom(["๐Ÿงฉ", "๐Ÿง ", "๐ŸŒ€", "โณ", "๐Ÿ”„"]); + await $.reply(text); + parentId = $.id; + } + const lookupId = `${$.author.id}_${parentId}`; + const client = await buildAzureClient(Bot); + const appKey = Bot.getSecret("APP_KEY") as string; + const modelName = Bot.getSecret("MODEL") as string; + + await StorageRef.init(); + const convoRecord = await StorageRef.getMemory(lookupId); + const conversationID = convoRecord?.conversationID || ""; + + const result = (await client.chat.completions.create({ + model: modelName, + messages: [ + { role: "system", content: SystemPrompt }, + { role: "user", content: JSON.stringify($.text) }, + ], + user: JSON.stringify({ + appkey: appKey, + ...(conversationID && { session_id: conversationID }), + }), + stop: ["<|im_end|>"], + stream: false, + })) as CustomAzureRes; + + const responseText = result.choices[0]?.message?.content; + if (responseText) { + await $.reply(responseText); + } + + if (!conversationID && result.user) { + try { + const userObj = JSON.parse(result.user) as { session_id?: string }; + if (typeof userObj.session_id === "string") { + await StorageRef.setMemory(lookupId, userObj.session_id); + } + } catch { + await $.send("There was an issue saving conversation memory"); + } + } + } + } catch (e) { + if (e && typeof e === "object" && "status" in e && e.status === 401) { + await $.send("It appears there is an issue with the system's API key"); + return $.end; + } + await $.send("It appears there was some type of catastrophic error"); + if ($.ctx.debug) { + console.log("Error", e); + await $.send(JSON.stringify(e)); + } + } + } + return $.next; +}); + +/** + * Example 3: "Streaming" + * LLM responses are streamed in, considering message edit limits in WebEx. + */ +Bot.addStep(async ($) => { + if ($.ctx.LLMStrategy === "streamExample") { + try { + if ($.text) { + const client = await buildAzureClient(Bot); + const appKey = Bot.getSecret("APP_KEY") as string; + const modelName = Bot.getSecret("MODEL") as string; + + const streamHandler = new LLMStream(); + const rootMsg = await $.send("Thinking..."); + + const result = await client.chat.completions.create({ + model: modelName, + messages: [ + { role: "system", content: SystemPrompt }, + { role: "user", content: $.text }, + ], + user: JSON.stringify({ appkey: appKey }), + stop: ["<|im_end|>"], + stream: true, + }); + + for await (const chunk of result) { + await streamHandler.incomingChunkHandler(chunk, async (textChunk) => { + await $.edit(rootMsg, textChunk); + return $.next; + }); + } + } + } catch (e) { + if (e && typeof e === "object" && "status" in e && e.status === 401) { + await $.send("It appears there is an issue with the system's API key"); + return $.end; + } + await $.send("It appears there was some type of catastrophic error"); + if ($.ctx.debug) { + console.log("Error", e); + await $.send(JSON.stringify(e)); + } + } + } + return $.next; +}); + +/** + * File Handling Example + * This step demonstrates how to handle file uploads and extract information from uploaded files. + */ +Bot.addStep(async ($) => { + if ($.file) { + const supportedExtensions = ["txt"]; + const { bytes, contentType, extension, name } = $.file; + + if (!supportedExtensions.includes(extension)) { + await $.send( + `This agent does not yet support file uploads of ${extension} files (${contentType})` + ); + + const bytesToMB = (bytes: number, integerOnly = false): number => { + const megabytes = bytes / (1024 * 1024); + return integerOnly ? Math.floor(megabytes) : +megabytes.toFixed(2); + }; + + await $.send( + `You uploaded "${name}", a *.${extension} file [${contentType}] with a size of ${bytesToMB( + bytes + )} MB [${bytes} bytes]` + ); + return $.end; + } + + if (extension === "txt") { + if (name === "andy.txt") { + await $.reply("๐Ÿ˜ป๐Ÿ˜ป"); + const fileData = (await $.file.getData()) as string; + + const client = await buildAzureClient(Bot); + const appKey = Bot.getSecret("APP_KEY") as string; + const modelName = Bot.getSecret("MODEL") as string; + + const result = await client.chat.completions.create({ + model: modelName, + messages: [ + { + role: "system", + content: `Here is the content of the text file (*.txt): ${fileData}`, + }, + { role: "user", content: `tell me about the text file contents` }, + ], + user: JSON.stringify({ appkey: appKey }), + stop: ["<|im_end|>"], + stream: false, + }); + + const responseText = result.choices[0]?.message?.content; + if (responseText) { + await $.send(responseText); + } + } + } + } + return $.next; +}); + +/** + * (optional): Restrict + scope access to specific people + * Populate allowedEmails with a list of permitted + * + */ +Bot.insertStepToFront(async ($) => { + const allowedEmails = [] as string[]; + if (allowedEmails.length) { + if (allowedEmails.includes($.author.email)) { + return $.next; + } + await $.send(`You do not have access to this agent`); + return $.end; + } + return $.next; +}); + +/** + * Azure Client Initialization + * Initialize the Azure OpenAI client, handle OAuth, etc + * Todo: persist the key and refresh if expires + */ +export const buildAzureClient = async ( + Bot: SpeedyBot +): Promise => { + const clientId = Bot.getSecret("CLIENT_ID") as string; + const clientSecret = Bot.getSecret("CLIENT_SECRET") as string; + const oAuthEndpoint = Bot.getSecret("OAUTH_ENDPOINT") as string; + const oauthClient = new OAuthHelper(clientId, clientSecret, oAuthEndpoint); + const apiKey = await oauthClient.getKey(); + const baseURL = Bot.getSecret("BASE_URL_LLM") as string; + const apiVersion = Bot.getSecret("API_VERSION") as string; + return new AzureOpenAI({ apiKey, apiVersion, baseURL }); +}; diff --git a/examples/azure/settings/doc_sample/andy.txt b/examples/azure/settings/doc_sample/andy.txt new file mode 100644 index 0000000..16e5a53 --- /dev/null +++ b/examples/azure/settings/doc_sample/andy.txt @@ -0,0 +1,3 @@ +My cat's name is Andy. + +Andy is a Russian Blue and a tiny bit grumpy. But if you give Andy treats he's a happy cat \ No newline at end of file diff --git a/examples/azure/settings/helpers/auth.ts b/examples/azure/settings/helpers/auth.ts new file mode 100644 index 0000000..6213bce --- /dev/null +++ b/examples/azure/settings/helpers/auth.ts @@ -0,0 +1,58 @@ +export class OAuthHelper { + constructor( + private clientId: string, + private clientSecret: string, + private tokenEndpoint: string + ) { + if (!this.tokenEndpoint) { + throw new Error( + "Missing tokenEndpoint, ex https://yourDomain.com/oauth2/default/v1/token" + ); + } + } + + private encodeClientCredentials( + clientId: string, + clientSecret: string + ): string { + const payload = `${clientId}:${clientSecret}`; + return typeof Buffer !== "undefined" + ? Buffer.from(payload).toString("base64") + : btoa(payload); + } + + public async getKey(): Promise { + const headers = { + Accept: "*/*", + "Content-Type": "application/x-www-form-urlencoded", + Authorization: `Basic ${this.encodeClientCredentials( + this.clientId, + this.clientSecret + )}`, + }; + + const body = new URLSearchParams({ + grant_type: "client_credentials", + }); + + try { + const response = await fetch(this.tokenEndpoint, { + method: "POST", + headers: headers, + body: body, + }); + + if (!response.ok) { + throw new Error( + `Error fetching access token: ${response.status} ${response.statusText}` + ); + } + + const data = await response.json(); + return data.access_token; + } catch (error) { + console.error("Error obtaining access token:", error); + throw error; + } + } +} diff --git a/examples/azure/settings/helpers/index.ts b/examples/azure/settings/helpers/index.ts new file mode 100644 index 0000000..be04a81 --- /dev/null +++ b/examples/azure/settings/helpers/index.ts @@ -0,0 +1,67 @@ +import { ChatCompletion } from "openai/resources"; +// Auth +export * from "./auth"; +// Streaming +export * from "./llm-stream"; +// Storage +export * from "./storage"; + +// Provides type (and typo) assistance when setting/fetching secrets from Bot.addSecret() +export type SecretsList = + | "OAUTH_ENDPOINT" + | "BASE_URL_LLM" + | "MODEL" + | "CLIENT_ID" + | "CLIENT_SECRET" + | "APP_KEY" + | "API_VERSION"; +interface Hate { + filtered: boolean; + severity: string; +} +interface Contentfilterresults { + hate: Hate; + self_harm: Hate; + sexual: Hate; + violence: Hate; +} +interface Message { + content?: string; + role: string; +} +interface Choice { + content_filter_results: Contentfilterresults; + finish_reason: string; + index: number; + message?: Message; +} +interface Promptfilterresult { + prompt_index: number; + content_filter_results: Contentfilterresults; +} +interface Usage { + completion_tokens: number; + prompt_tokens: number; + total_tokens: number; +} + +export type CustomAzureRes = ChatCompletion & { + prompt_sent_to_chatgpt: Message[]; + user?: string; + prompt_filter_results: Promptfilterresult; +}; +export type AzureRes = { + choices: Choice[]; + created: number; + id: string; + model: string; + object: string; + prompt_filter_results: Promptfilterresult[]; + system_fingerprint?: any; + usage: Usage; + prompt_sent_to_chatgpt: Message[]; + user?: string; +}; + +export type LLMSamples = "simpleExample" | "threadsExample" | "streamExample"; +// | "personaExample"; diff --git a/examples/azure/settings/helpers/llm-stream.ts b/examples/azure/settings/helpers/llm-stream.ts new file mode 100644 index 0000000..b8bb734 --- /dev/null +++ b/examples/azure/settings/helpers/llm-stream.ts @@ -0,0 +1,41 @@ +// Could swap out for Anthropic or any provider +import openai from "openai"; + +type ChunkCallback = ( + textChunk: string, + isFinished?: boolean +) => Promise; + +export class LLMStream { + private editsDone: number = 0; + private maxEdits: number; + private minWords: number; + private charBuffer: string = ""; // Full buffer + private subCharBuffer: string = ""; // Sub buffer for the current chunk + private isFinished: boolean = false; + + constructor(config?: { maxEdits?: number; minWords?: number }) { + this.maxEdits = config?.maxEdits ?? 9; + this.minWords = config?.minWords ?? 15; + } + + public async incomingChunkHandler( + chunk: openai.Chat.ChatCompletionChunk, + callback: ChunkCallback + ): Promise { + const text = chunk.choices[0]?.delta?.content || ""; + this.charBuffer += text; + this.subCharBuffer += text; + this.isFinished = chunk.choices[0]?.finish_reason === "stop"; + const numWords = this.subCharBuffer.split(" ").length; + if ( + this.isFinished || + (this.editsDone < this.maxEdits && numWords >= this.minWords) + ) { + await callback(this.charBuffer, this.isFinished); + this.editsDone++; + this.subCharBuffer = ""; + } + return true; + } +} diff --git a/examples/azure/settings/helpers/prompts.ts b/examples/azure/settings/helpers/prompts.ts new file mode 100644 index 0000000..1d99aab --- /dev/null +++ b/examples/azure/settings/helpers/prompts.ts @@ -0,0 +1,15 @@ +// Customize as needed +export const SystemPrompt = ` +=== Mission +Your role is to assist users by providing clear, direct, and helpful responses. Follow the tone specified by the user and focus on their needs. + +=== Response Guidelines +Be Exact: Give users what they ask for. +Be Helpful: Provide the best answers and solutions. +Be Engaging: Respond with enthusiasm and offer relevant info. +Be Direct: Address requests head-on without unnecessary details. +Be Friendly and Professional: Be polite, professional, and approachable. +Be Accurate: Provide correct information and suggest further resources if needed. + +Support users effectivelyโ€”avoid chit-chat, donโ€™t preach, and follow user instructions. +`; diff --git a/examples/azure/settings/helpers/storage.ts b/examples/azure/settings/helpers/storage.ts new file mode 100644 index 0000000..a3b44db --- /dev/null +++ b/examples/azure/settings/helpers/storage.ts @@ -0,0 +1,150 @@ +import { Database } from "sqlite3"; // Swap out w/ serious/thoughtful storage when deploying + +export type ConversationMemoryPayload = { + conversationID: string; + totalTokens?: number; +}; +// Base class to handle database operations +abstract class BaseStorage { + protected db: Database; + protected tableName: string; + + constructor(dbPath?: string, tableName?: string) { + this.db = new Database(dbPath || ":memory:"); + this.tableName = tableName || "default_table"; + } + + protected async runQuery(query: string, params?: any[]): Promise { + return new Promise((resolve, reject) => { + this.db.run(query, params, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } + + protected async getQuery(query: string, params?: any[]): Promise { + return new Promise((resolve, reject) => { + this.db.get(query, params, (err, row) => { + if (err) { + reject(err); + } else { + resolve(row); + } + }); + }); + } + + protected async allQuery(query: string, params?: any[]): Promise { + return new Promise((resolve, reject) => { + this.db.all(query, params, (err, rows) => { + if (err) { + reject(err); + } else { + resolve(rows); + } + }); + }); + } + + protected abstract createTable(): Promise; + + async init(): Promise { + await this.createTable(); + } +} + +// SimpleMemoryStorage: Maps special userId to conversationID and optional totalTokens +export class SimpleMemoryStorage extends BaseStorage<{ + conversationID: string; + totalTokens?: number; +}> { + private initReady = false; + + constructor(dbPath = ":memory:") { + super(dbPath, "simple_memory"); + } + + protected async createTable(): Promise { + if (!this.initReady) { + const query = ` + CREATE TABLE IF NOT EXISTS ${this.tableName} ( + userId TEXT PRIMARY KEY, + conversationID TEXT, + totalTokens INTEGER + ) + `; + await this.runQuery(query); + } + } + + async setMemory( + userId: string, + conversationID: string, + totalTokens?: number + ): Promise { + const query = ` + INSERT INTO ${this.tableName} (userId, conversationID, totalTokens) VALUES (?, ?, ?) + ON CONFLICT(userId) DO UPDATE SET conversationID=excluded.conversationID, totalTokens=excluded.totalTokens + `; + await this.runQuery(query, [userId, conversationID, totalTokens]); + } + + async getMemory( + userId: string + ): Promise { + const query = `SELECT conversationID, totalTokens FROM ${this.tableName} WHERE userId = ?`; + const row = await this.getQuery(query, [userId]); + if (row) { + return { + conversationID: row.conversationID, + totalTokens: row.totalTokens, + }; + } + return undefined; + } +} + +// ConversationHistoryStorage: Stores OpenAI/ChatGPT message history payload +interface Message { + role: string; + content: string; +} + +// Save lists of { role: string, content: string} for context +export class ConversationHistoryStorage extends BaseStorage { + constructor(dbPath?: string) { + super(dbPath, "conversation_history"); + } + + protected async createTable(): Promise { + const query = ` + CREATE TABLE IF NOT EXISTS ${this.tableName} ( + userId TEXT PRIMARY KEY, + history TEXT + ) + `; + await this.runQuery(query); + } + + async setHistory(userId: string, history: Message[]): Promise { + const serializedHistory = JSON.stringify(history); + const query = ` + INSERT INTO ${this.tableName} (userId, history) VALUES (?, ?) + ON CONFLICT(userId) DO UPDATE SET history=excluded.history + `; + await this.runQuery(query, [userId, serializedHistory]); + } + + async getHistory(userId: string): Promise { + const query = `SELECT history FROM ${this.tableName} WHERE userId = ?`; + const row = await this.getQuery(query, [userId]); + if (row) { + return JSON.parse(row.history); + } + return undefined; + } +} diff --git a/examples/azure/tsconfig.json b/examples/azure/tsconfig.json new file mode 100644 index 0000000..7a2427c --- /dev/null +++ b/examples/azure/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "declaration": true, + "resolveJsonModule": true, + "module": "commonjs", + "target": "ES2016", + "noImplicitAny": false, + "moduleResolution": "node", + "removeComments": true, + "sourceMap": true, + "skipLibCheck": true, + "strict": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": false, + "noUnusedLocals": true, + "outDir": "dist", + "baseUrl": ".", + "forceConsistentCasingInFileNames": true, + "paths": { + "*": ["node_modules/*"] + } + }, + "include": ["util/**/*", "package.json"], + "exclude": ["node_modules", "dist", "@vitest"] +} diff --git a/examples/azure/util/cli.ts b/examples/azure/util/cli.ts new file mode 100644 index 0000000..8e1cdd2 --- /dev/null +++ b/examples/azure/util/cli.ts @@ -0,0 +1,145 @@ +// #!/usr/bin/env node +import * as dotenv from "dotenv"; +import { resolve } from "path"; +import * as fs from "fs/promises"; +import * as readline from "readline"; +import { botTokenKey, logoRoll } from "speedybot"; +import { LocalSockets } from "./websockets"; + +// Expects .env to get token on BOT_TOKEN +dotenv.config({ path: resolve(__dirname, "..", ".env") }); + +async function writeEnvFile(envObject, append = true) { + try { + const envContents = Object.entries(envObject) + .map(([key, value]) => `${key}=${value}`) + .join("\n"); + const flag = append ? "a" : "w"; // Use "a" for append, "w" for write (overwrite) + await fs.writeFile(".env", append ? `\n${envContents}` : envContents, { + flag, + }); + } catch (error) { + console.error("Error writing to .env:", error); + process.exit(1); + } +} + +async function promptUser(msg) { + return new Promise((resolve) => { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + rl.question(`${msg}: `, (answer) => { + rl.close(); + resolve(answer); + }); + }); +} + +async function resetBot(token) { + let finalToken = token || process.env[botTokenKey]; + if (!finalToken) { + finalToken = await promptUser( + "Enter your token, [Usage: npm run bot:reset ]" + ); + } + + const websockets = new LocalSockets(finalToken); + await websockets.resetDevices(); + console.log("Reset attempted"); +} + +async function writeToken(token) { + let finalToken = token; + if (!token) { + finalToken = await promptUser( + `Enter your token, [Usage: npm run bot:setup ]` + ); + } + // destructively overwrites .env as BOT_TOKEN=xxxxxxxx + const envObject = { [botTokenKey]: finalToken }; + await writeEnvFile(envObject, false); + console.log( + `[โœ… SpeedyBot] .env written to ${resolve(__dirname, "..", ".env")} ` + ); +} + +/** + * + * @param keyValue: keyName=TheValue + */ +async function writeSecret(keyValue: string) { + if (!keyValue || (keyValue && !keyValue.includes("="))) { + console.log(`โŒ[ERROR] You need to add a secret, ex`); + console.log(` + npm run bot:addsecret SECRETNAME=secret12345678 + `); + process.exit(1); + } + // appends .env as key=value + const [key, value] = keyValue.split("="); + const envObject = { [key]: value }; + await writeEnvFile(envObject, true); + console.log( + `[โœ… SpeedyBot] .env written to ${resolve(__dirname, "..", ".env")} ` + ); +} +async function run(command, ...args) { + switch (command) { + case "reset": + await resetBot(args[0]); + break; + case "setup": + await writeToken(args[0]); + break; + case "addsecret": + await writeSecret(args[0]); + break; + case "help": + console.log(await logoRoll()); + console.log("Project Path:", process.cwd()); + console.log(` +Usage: + $ npm run bot:debug + $ npm run bot:dev + $ npm run bot:on + $ npm run bot:reset + $ npm run bot:setup + $ npm run bot:addsecret MYKEY=THE_VALUE + +Commands: + 1. Debug Command: + $ npm run bot:debug + Display debug information about your system + + 2. Development Command: + $ npm run bot:dev + Start your bot locally (using websockets) with live-reload on code-changes + + 3. Online Command: + $ npm run bot:on + "Turn on" your bot locally (over websockets) but without live-reload on code-changes, CTRL-C to exit + + 4. Reset Command: + $ npm run bot:reset + Reset any devices, useful for rate limit situations + + 5. Setup Command: + $ npm run bot:setup + Overwrite (delete all other values) in .env file with BOT_TOKEN= + + 6. Add Secret + $ npm run bot:addsecret MYKEY=THE_VALUE + Append an .env file with MYKEY=THE_VALUE +`); + break; + default: + console.error(`Unknown command: ${command}`); + break; + } +} + +const [, , command, ...args] = process.argv; +run(command, ...args); diff --git a/examples/azure/util/index.ts b/examples/azure/util/index.ts new file mode 100644 index 0000000..99b6643 --- /dev/null +++ b/examples/azure/util/index.ts @@ -0,0 +1,63 @@ +import { resolve } from "path"; +import { SecretsList } from "../settings/helpers/index"; + +declare const Bun: unknown; +export const isBun = () => { + return typeof Bun !== "undefined"; +}; + +export const announceExit = (name?: string) => { + const isColorSupported = process.stdout.isTTY; + if (isColorSupported) { + const BOT_DISCONNECTED = `\n\x1b[1m\x1b[7m\x1b[31m ๐Ÿค– DISCONNECTED \x1b[0m\x1b[31m Bot is now offline. \x1b[0m`; + process.stdout.write(BOT_DISCONNECTED + "\n"); + } else { + console.log("Bot is now offline."); + } + console.log( + ` + ${name && typeof name === "string" ? name : "Your bot"} is now "off" + + You can turn your bot back on by entering the following commands: + + cd ${resolve(__dirname, "..")} + ${isBun() ? "bun --watch util/launch.ts" : "npm run dev"} + + If you want to deploy your bot to a persistent server or serverless function, see here: + https://speedybot.js.org/examples + ` + ); +}; + +// Modify below +// Note: Requires secrets to be mounted in environment +export const getSecrets = (): Record => { + const secrets: Partial> = { + OAUTH_ENDPOINT: process.env.OAUTH_ENDPOINT, + BASE_URL_LLM: process.env.BASE_URL_LLM, + MODEL: process.env.MODEL, + CLIENT_ID: process.env.CLIENT_ID, + CLIENT_SECRET: process.env.CLIENT_SECRET, + APP_KEY: process.env.APP_KEY, + API_VERSION: process.env.API_VERSION, + }; + + const missingSecrets = Object.keys(secrets).filter( + (key) => secrets[key as SecretsList] === undefined + ) as SecretsList[]; + + if (missingSecrets.length > 0) { + throw new Error(`Missing secrets: ${missingSecrets.join(", ")}`); + } + + if ( + secrets["OAUTH_ENDPOINT"] === + "https://yourDomain.com/oauth2/default/v1/token" + ) { + throw new Error( + "Placeholder configuration detected, replace placeholders in .env with real values and try again" + ); + } + + return secrets as Record; +}; diff --git a/examples/azure/util/launch.ts b/examples/azure/util/launch.ts new file mode 100644 index 0000000..fca468c --- /dev/null +++ b/examples/azure/util/launch.ts @@ -0,0 +1,20 @@ +import "cross-fetch/polyfill"; +import { config } from "dotenv"; +import { resolve } from "path"; +import { announceExit, getSecrets } from "./index"; +import { SpeedySockets } from "./websockets"; +import Bot from "./../settings/bot"; + +// Setup process event handlers +process.on("exit", announceExit); +process.on("SIGINT", () => process.exit(0)); + +// Load environment variables +config({ path: resolve(__dirname, "..", ".env") }); + +// Initialize Bot with azure secrets + config and token +Bot.addSecrets(getSecrets()); +Bot.setToken(process.env.BOT_TOKEN as string); + +// Start WebSocket connection +SpeedySockets(Bot, { force: false, debug: false }).catch(console.log); diff --git a/examples/azure/util/types.ts b/examples/azure/util/types.ts new file mode 100644 index 0000000..c7fe7d5 --- /dev/null +++ b/examples/azure/util/types.ts @@ -0,0 +1,13 @@ +// Customize for your implementation +export type AuthConfig = { + clientId: string; + clientSecret: string; + oAuthEndpoint: string; +}; + +export type LLMConfig = { + appKey: string; + stream: boolean; + memory: boolean; // do we want to worry about + endpoint: string; +}; diff --git a/examples/azure/util/websockets.ts b/examples/azure/util/websockets.ts new file mode 100644 index 0000000..c7a89f0 --- /dev/null +++ b/examples/azure/util/websockets.ts @@ -0,0 +1,659 @@ +// Author: @valgaze +// Websockets are super useful but painful to use sometimes +// use bun or install Websocket with: +// $ npm install ws +import WebSocket from "ws"; // optional + +import { + type AA_Envelope, + type File_Details, + type MessageEnvelope, + type Message_Details, + type Submit_Details, + type SelfData, + SpeedyBot, +} from "speedybot"; + +import { logoRoll } from "speedybot"; + +// all the things that can go wrong :( +enum ErrorType { + HTTP_REQUEST_FAILED = "HTTP_REQUEST_FAILED", + WEBSOCKET_ERROR = "WEBSOCKET_ERROR", + INVALID_TOKEN = "INVALID_TOKEN", // 401 + EXCESSIVE_DEVICE = "EXCESSIVE_DEVICE", + UNKNOWN_ERROR = "UNKNOWN_ERROR", + FORBIDDEN_ERROR = "ERROR_FORBIDDEN", +} + +export type AbbeviatedDevice = { + url: string; + webSocketUrl: string; + services: unknown; + deviceType: string; + name: string; + model: string; + localizedModel: string; + systemName: string; + systemVersion: string; + capabilities: unknown; + features: unknown; + creationTime: string; + modificationTime: string; + deviceSettings: unknown; + deviceSettingsString: string; + showSupportText: boolean; + reportingSiteUrl: string; + reportingSiteDesc: string; + isDeviceManaged: boolean; + trainSiteNames: unknown[]; + clientSecurityPolicy: string; + intranetInactivityCheckUrl: string; + serviceHostMap: unknown; + blockExternalCommunications: boolean; + desktopFileShareControl: string; + mobileFileShareControl: string; + webFileShareControl: string; + botFileShareControl: string; + whiteboardFileShareControl: string; + clientMessagingGiphy: string; + clientMessagingLinkPreview: string; + ecmEnabledForAllUsers: boolean; + ecmSupportedStorageProviders: string[]; + defaultEcmMicrosoftCloud: string; + ecmMicrosoftTenant: string; + ecmScreenCaptureFeatureAllowed: boolean; + ecmWhiteboardFileDataAllowed: boolean; + callingBehavior: string; + onPremisePairingEnabled: boolean; + peopleInsightsEnabled: boolean; + allowSelfSignedCertificate: boolean; + webexCrossLaunch: boolean; + webexAppHubEnabled: boolean; + embeddedAppsEnabled: boolean; + settings: unknown; + selfSignupOrg: boolean; + userId: string; + orgId: string; + orgName: string; +}; + +export type Events = "text" | "card"; + +export type Data = { + [key: string]: any; +}; +export type BaseEnvelope = { + id: string; + name: string; + targetUrl: string; + resource: string; + event: string; + orgId: string; + createdBy: string; + appId: string; + ownedBy: string; + status: string; + created: Date; + actorId: string; + data: Data; +}; +export type MessagePayload = { + id: string; + timestamp: string | number; + data: { + eventType: + | "status.start_typing" // for first-press situations + | "conversation.activity" + | "conversation.highlight"; + activity?: { + id: string; + verb: "cardAction" | "post" | "acknowledge" | "update" | "share"; + actor: { + id: string; // email + objectType: string; + displayName: string; + orgId: string; + emailAddress: string; + entryUUID: string; + type: string; + }; + target: { + id: string; + objectType: string; + url: string; + published: string; + participants: { + items: any[]; + }; + activities: { + items: any[]; + }; + deletedActivityIds: any[]; + tags: string[]; + globalId: string; + }; + }; + }; +}; +export type SocketConfig = { debug: boolean; force?: boolean }; + +/** + * config: + * debug: show logs (noisy) + * force: force a new websocket connection each time (might cause excessive device registration) + */ +export class LocalSockets { + private submitFcn!: (AAEnv: AA_Envelope) => Promise | void; + onSubmit(cb: (AAEnv: AA_Envelope) => Promise | void) { + this.submitFcn = cb; + } + + private msgFcn!: (MsgEnv: MessageEnvelope) => Promise | void; + onText(cb: (MsgEnv: MessageEnvelope) => Promise | void) { + this.msgFcn = cb; + } + + private ws!: WebSocket; + constructor( + private _token: string, + private config: SocketConfig = { + debug: false, + force: false, + } + ) { + this.root = { + DEVICES_URL: "https://wdm-a.wbx2.com/wdm/api/v1/devices", + DEVICE_DATA: { + deviceName: "SpeedyBot-websockets", + deviceType: "DESKTOP", + localizedModel: "SpeedyBot", + model: "SpeedyBot", + name: this.config.force + ? `speedy-webex-${Math.random().toString(36).slice(2, 7)}` + : "speedybot-webex-reusable-websocket", + systemName: "SpeedyBot", + systemVersion: "1.0", + }, + }; + } + + private root: { + DEVICES_URL: string; + DEVICE_DATA: { + deviceName: string; + deviceType: string; + localizedModel: string; + model: string; + name: string; + systemName: string; + systemVersion: string; + }; + }; + + public log(...payload: any): void { + if (this.config.debug) + console.log.apply(console, [ + "[Speedy-Debug]", + ...(payload as [any?, ...any[]]), + ]); + } + + async start(): Promise { + this.log(`Getting device info...`); + this.log(this.root.DEVICE_DATA.name); + let device = await this.getDeviceInfo(); + if (!device || this.config.force) { + this.log( + `${ + this.config.force + ? "Force new device creation..." + : "Empty device refetching..." + }` + ); + device = await this.createDevice(); + this.log(`Device set`); + } + + if (this.ws) { + this.log(`Attempting socket disconnect`); + await this.disconnect(); + } + + if (device && "webSocketUrl" in device) { + this.log(`Registering websockets with + ${device.webSocketUrl}`); + this.log(`Device name: ${device.name}`); + await this.registerWebsockets(device.webSocketUrl); + this.log(`โœ… websockets success`); + return true; + } else { + this.log(`๐Ÿ›‘ websockets failure`); + return false; + } + } + + private buildUUID() { + return Math.random().toString(36).slice(2); + } + + handleApplicationError(error: any, errorType: ErrorType, message?: string) { + switch (errorType) { + case ErrorType.HTTP_REQUEST_FAILED: + console.error("HTTP request failed:", message, error); + break; + case ErrorType.WEBSOCKET_ERROR: + console.error("WebSocket error:", message, error); + break; + case ErrorType.INVALID_TOKEN: + console.error("Invalid token error:", message, error); + break; + case ErrorType.EXCESSIVE_DEVICE: + console.error("Excessive device registration error:", message, error); + break; + default: + console.error("Unknown error:", message, error); + break; + } + } + + private async createDevice(): Promise { + try { + const response = await fetch(this.root.DEVICES_URL, { + method: "POST", + headers: { + Authorization: `Bearer ${this._token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(this.root.DEVICE_DATA), + }); + + if (!response.ok) { + if (response.status === 429 || response.status === 403) { + /** + * + * + { + message: "User has excessive device registrations", + errors: [ + { + description: "User has excessive device registrations", + } + ], + trackingId: "xxxxx", +} + * + */ + + throw new Error(ErrorType.EXCESSIVE_DEVICE); + } else { + throw new Error(ErrorType.UNKNOWN_ERROR); + } + } + const device = (await response.json()) as AbbeviatedDevice; + return device; + } catch (error: unknown) { + if (typeof error === "object" && error && "message" in error) { + if (error.message === ErrorType.EXCESSIVE_DEVICE) { + this.handleApplicationError( + error, + ErrorType.EXCESSIVE_DEVICE, + "WebSocket connection error" + ); + } else if (error.message === ErrorType.FORBIDDEN_ERROR) { + this.handleApplicationError( + error, + ErrorType.FORBIDDEN_ERROR, + "403 forbidden" + ); + } else { + this.handleApplicationError( + error, + ErrorType.UNKNOWN_ERROR, + "Unknown error on creation" + ); + } + } + } + } + + private async getDeviceInfo(): Promise { + try { + const response = await fetch(this.root.DEVICES_URL, { + headers: { + Authorization: `Bearer ${this._token}`, + }, + }); + + if (!response.ok && response.status === 401) { + throw new Error(ErrorType.INVALID_TOKEN); + } + + const data = (await response.json()) as + | { devices: AbbeviatedDevice[] } + | { + message: string; + errors: { description: string }[]; + trackingId: string; + }; + if ("devices" in data) { + this.log(`Total devices: ${data.devices.length}`); + const device = data.devices.find( + (device: any) => device.name === this.root.DEVICE_DATA.name + ); + return device; + } + } catch (error) { + if (typeof error === "object" && error && "message" in error) { + this.handleApplicationError( + error, + ErrorType.INVALID_TOKEN, + `[โŒ SpeedyBot] Token invalid-- double check the token is correct or you can regenerate a new one here: +https://developer.webex.com/my-apps + +` + ); + } + } + } + + public async resetDevices() { + type Device = { + url: string; + webSocketUrl: string; + services: unknown; + }; + + try { + const deviceListResponse = await fetch( + "https://wdm-a.wbx2.com/wdm/api/v1/devices", + { + headers: { + Authorization: `Bearer ${this._token}`, + }, + } + ); + + if (!deviceListResponse.ok) { + if (deviceListResponse.status === 401) { + this.log(`Reset devices invalid token`); + throw new Error(ErrorType.INVALID_TOKEN); + } + throw new Error(ErrorType.HTTP_REQUEST_FAILED); + } + + const { devices = [] }: { devices: Device[] } = + (await deviceListResponse.json()) as { devices: Device[] }; + + for (const device of devices) { + const { url } = device; + if (url) { + const deleteResponse = await fetch(url, { + method: "DELETE", + headers: { + Authorization: `Bearer ${this._token}`, + }, + }); + + if (!deleteResponse.ok) { + throw new Error(ErrorType.HTTP_REQUEST_FAILED); + } + } + } + + return true; + } catch (error) { + if (error instanceof Error) { + if (error.message === ErrorType.HTTP_REQUEST_FAILED) { + this.handleApplicationError( + error, + ErrorType.HTTP_REQUEST_FAILED, + "Failed to reset devices" + ); + } else if (error.message === ErrorType.INVALID_TOKEN) { + this.handleApplicationError( + error, + ErrorType.INVALID_TOKEN, + `[โŒ SpeedyBot] Token invalid-- double check the token is correct or you can regenerate a new one here: +https://developer.webex.com/my-apps + + ` + ); + } else { + this.handleApplicationError( + error, + ErrorType.UNKNOWN_ERROR, + "Unknown error while resetting devices" + ); + } + } + return false; + } + } + + public registerWebsockets(webSocketUrl: string) { + this.ws = new WebSocket(webSocketUrl); + this.ws.on("open", async () => { + this.log("๐ŸŽธ Websocket open"); + const msg = { + // id: this.buildUUID(), + id: "myrando1243", + type: "authorization", + data: { + token: `Bearer ${this._token}`, + }, + }; + this.ws.send(JSON.stringify(msg)); + }); + + this.ws.on("message", async (message: string) => { + try { + const msg = JSON.parse(message); + this.log(`๐Ÿ“ฌ raw message received ${JSON.stringify(msg)}\n`); + await this.processMessage(msg); + } catch (error: unknown) { + if (typeof error === "object" && error && "message" in error) { + this.handleApplicationError( + error, + ErrorType.WEBSOCKET_ERROR, + "WebSocket error on message" + ); + } + } + }); + + this.ws.on("error", (error) => { + this.handleApplicationError( + error, + ErrorType.WEBSOCKET_ERROR, + "WebSocket connection error" + ); + }); + } + + public disconnect() { + if (this.ws) { + return this.ws.close(); + } + } + + private async checkEmail(email: string) { + const emailRes = await fetch("https://webexapis.com/v1/people/me", { + headers: { + Authorization: `Bearer ${this._token}`, + }, + }); + const { emails } = (await emailRes.json()) as SelfData; + const [myEmail] = emails; + return email !== myEmail; + } + + private async fetchMessagePayload(id: string): Promise { + const messageId = id.includes("-") + ? Buffer.from(`ciscospark://us/MESSAGE/${id}`).toString("base64") + : id; + + const response = await fetch( + `https://webexapis.com/v1/messages/${messageId}`, + { + headers: { + Authorization: `Bearer ${this._token}`, + }, + } + ); + const data = (await response.json()) as Message_Details; + return data; + } + + private async fetchCardPayload(id: string): Promise { + const attachmentActionId = id.includes("-") + ? Buffer.from(`ciscospark://us/ATTACHMENT_ACTION/${id}`).toString( + "base64" + ) + : id; + const response = await fetch( + `https://webexapis.com/v1/attachment/actions/${attachmentActionId}`, + { + headers: { + Authorization: `Bearer ${this._token}`, + }, + } + ); + const data = (await response.json()) as Promise; + return data; + } + + async processMessage(msg: MessagePayload): Promise { + if ( + msg.data.eventType === "conversation.activity" && + "activity" in msg.data && + msg.data.activity + ) { + const { id, verb, actor } = msg.data.activity; + const { emailAddress } = actor; + const proceed = await this.checkEmail(emailAddress); + if (!proceed) return false; // filter out mesages from agent + + // text [post] or file [share] message + if (verb === "post" || verb === "share") { + if (this.msgFcn) { + const payload = await this.fetchMessagePayload(id); + this.log(`[${verb}]`, id, payload); + const env = this.buildText(payload); + await this.msgFcn(env); + } + } + + // if adaptive card/attachmentAction submission [cardAction] + if (verb === "cardAction") { + if (this.submitFcn) { + const payload = await this.fetchCardPayload(id); + this.log("[cardAction]", payload); + const env = this.buildCard(payload); + await this.submitFcn(env); + } + } + } + + return true; + } + + setToken(token: string) { + this._token = token; + } + + getToken(): string { + return this._token; + } + + private buildText(payload: Message_Details | File_Details): MessageEnvelope { + return { + id: "__websocket_id", + name: "__websocket_name", + targetUrl: "__websocket_targetUrl", + resource: "messages", + event: "created", + orgId: "__websocket_orgId", + createdBy: "__websocket_createdBy", + appId: "__websocket_appId", + ownedBy: "creator", + status: "active", + created: new Date().toISOString(), + actorId: "__websocket_actorId", + data: payload, + }; + } + + private buildCard(payload: Submit_Details): AA_Envelope { + return { + id: "__websocket_id", + name: "__websocket_name", + targetUrl: "__websocket_targetUrl", + resource: "attachmentActions", + event: "created", + orgId: "__websocket_orgId", + createdBy: "__websocket_createdBy", + appId: "__websocket_appId", + ownedBy: "creator", + status: "active", + created: new Date().toISOString(), + actorId: "__websocket_actorId", + data: payload, + }; + } +} + +export const announceWebsockets = (email: string, name = "Your bot") => { + const isColorSupported = process.stdout.isTTY; + console.log(logoRoll()); + if (isColorSupported) { + const WEBSOCKETS_READY = `\x1b[1m\x1b[7m\x1b[32m ๐ŸŒ CONNECTED \x1b[0m\x1b[32m Websockets active, listening...\x1b[0m`; + process.stdout.write(WEBSOCKETS_READY + "\n"); + } else { + console.log("Websockets Registered. Listening..."); + } + console.log(`You can reach ${name} here: ${email}`); +}; + +/** + * Helper to surround SpeedyBot instance with websockets + * + */ +export async function SpeedySockets( + BotRef: SpeedyBot, + config: SocketConfig = { + debug: false, + force: false, + }, + cb?: (data?: { email: string; name?: string }) => any +): Promise { + try { + const token = BotRef.getToken(); + if (!token) { + throw new Error("No token provided (try Bot.setToken('__REPLACE__ME__')"); + } + const inst = new LocalSockets(token, config); + + inst.onSubmit(async (payload) => { + BotRef.runMiddleware(payload); + }); + + inst.onText((payload) => { + BotRef.runMiddleware(payload); + }); + + await inst.start(); + const data = await BotRef.getSelf(); + const { displayName } = data; + const [email] = data.emails; + + if (!cb) { + announceWebsockets(email, displayName); + } else { + return cb({ email, name: displayName }); + } + } catch (e) { + console.log(e); + throw e; + } +} diff --git a/examples/standard-server/README.md b/examples/standard-server/README.md index 5312690..271a261 100644 --- a/examples/standard-server/README.md +++ b/examples/standard-server/README.md @@ -52,10 +52,12 @@ curl -X POST -H "Content-Type: application/json" -d '{"id": 1234567890987654321} -## 6) Supply your Webhook "secret" to your Worker +## 6) Supply your Webhook "secret" to your server Even though it's "optional", it's a really, really good idea to set a Webhook Secret too so you can make sure incoming requests are the real deal. For more detail, see **[https://speedybot.js.org/webhooks#securing-webhooks](https://speedybot.js.org/webhooks#securing-webhooks)** +To add a secret to an existing webhook, delete the current webhook and create a new one with the secret. + ## NPM Run Scripts All you'll probably need are `npm run bot:dev` + maybe `npm run bot:reset` diff --git a/examples/standard-server/src/index.ts b/examples/standard-server/src/index.ts index d6c91bb..8441ce2 100644 --- a/examples/standard-server/src/index.ts +++ b/examples/standard-server/src/index.ts @@ -21,7 +21,7 @@ if (!token) { } Bot.setToken(token); -// Replace TARGET below with an email address of room id +// Replace TARGET below with an email address or room id const TARGET = "joe@joe.com"; app.post("/speedybot", async (req, res) => { diff --git a/package.json b/package.json index abf3ea1..c3b2858 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "speedybot", - "version": "2.0.5", + "version": "2.0.6", "main": "dist/cjs/src/index.js", "module": "dist/mjs/src/index.js", "types": "dist/mjs/src/index.d.ts", diff --git a/src/index.ts b/src/index.ts index 03b43e6..720d408 100644 --- a/src/index.ts +++ b/src/index.ts @@ -99,6 +99,9 @@ export const mainRequester = async ( try { const response = await fetch(url, init); if (!response.ok) { + console.log("Full", { init, response }); + const beer = await response.text(); + console.log("#", beer); throw new RequestError( `The request to ${url} failed with status ${response.status}${ response.status === 401 diff --git a/src/speedybot.ts b/src/speedybot.ts index c331ee7..974e755 100644 --- a/src/speedybot.ts +++ b/src/speedybot.ts @@ -437,15 +437,13 @@ export class SpeedyBot { await botInst._send(msgObj); } return true; - - // type Thread = [root: string | SpeedyCard, ...messages: Array] & { length: 1 | 2 | 3 | 4 5}; }, send(msg: Message) { return botInst.sendTo(roomId, msg); }, reply(msg: string) { - return botInst.replyTo(roomId, messageId, msg); + return botInst.replyTo(roomId, this.msg.parentId ?? messageId, msg); }, edit(messageObj: MessageResponse, msg: string) { return botInst.editMessage(messageObj.roomId, messageObj.id, msg); @@ -812,7 +810,7 @@ ${type === "json" ? JSON.stringify(data, null, 2) : data} } public async replyTo( - param1: MessageResponse | string, + roomIdParam: MessageResponse | string, param2: string | undefined, param3?: string ): Promise { @@ -820,14 +818,14 @@ ${type === "json" ? JSON.stringify(data, null, 2) : data} let messageId: string; let msg: string; - if (typeof param1 === "object") { - // Handle the overload when param1 is an object (MessageResponse) - roomId = param1.roomId; // Use the roomId from param1 - messageId = param1.id; // Use the messageId from param1 + if (typeof roomIdParam === "object") { + // Handle the overload when roomIdParam is an object (MessageResponse) + roomId = roomIdParam.roomId; // Use the roomId from roomIdParam + messageId = roomIdParam.id; // Use the messageId from roomIdParam msg = param2 || ""; // Use param2 as the message, default to an empty string if not provided } else { - // Handle the overload when param1 is a string - roomId = param1; // Use param1 as the roomId + // Handle the overload when roomIdParam is a string + roomId = roomIdParam; // Use roomIdParam as the roomId messageId = param2 || ""; // Use param2 as the messageId, default to an empty string if not provided msg = param3 || ""; // Use param3 as the message, default to an empty string if not provided }