Compare commits

...

25 Commits

Author SHA1 Message Date
Slavi Pantaleev
2dbd600d05 Release 1.0.6 2024-09-19 14:03:00 +03:00
Slavi Pantaleev
f6cc8363d1 Allow regular (unprivileged) users to see providers help and adapt it to them 2024-09-19 13:39:52 +03:00
Slavi Pantaleev
e3b07aa291 Improve bot make-use-of-me steps in introduction message
This especially improves the introduction for when the bot has no
configured agents.
2024-09-19 12:24:03 +03:00
Slavi Pantaleev
324c8a976f Relocate some access check code 2024-09-19 10:02:23 +03:00
Slavi Pantaleev
fb1f16aa40 Add missing new line before closing code-block 2024-09-18 09:15:23 +03:00
Slavi Pantaleev
012069891d Adjust incorrect comment
[skip ci]
2024-09-18 08:53:33 +03:00
Slavi Pantaleev
3b7c28a55e Release 1.0.5 2024-09-14 10:47:56 +03:00
Slavi Pantaleev
3b25b92a81 Implement more fine-grained typing notices sending
The previous approach (implemented in dd1dd78312) was simple
(send typing notices for as long as the "controller" is running),
but this proved to be overly simplistic and unable to handle edge-cases:

- in multi-user rooms (or rooms with a prefix requirement), the bot
  used to send a typing notice while "working", but its work consisted
  of ignoring the message. So it then sent a "not typing" notice.
  This is wasteful and otherwise problematic - certain clients (like nheko)
  do not handle this "race" well.

- certain reactions (anything other than 🗣️ right now) are meant to be
  ignored. There's no point in doing the same "typing / not typing"
  dance

- there are other instances where the bot may do work, but doesn't (due
  to configuration or lack of capabilities)

This new more fine-grained implementation of typing notices aims to:

- only send a typing notice if actual "slow work" will be done

- avoid stopping & restarting typing notices (wasteful) if a chain of work is to
  be performed (processing voice messages and doing speech-to-text +
  text-generation + ...). Rather, maintaining typing notice sending
  throughout
2024-09-14 10:39:20 +03:00
Slavi Pantaleev
509f683365 Fix typo 2024-09-14 09:30:58 +03:00
Slavi Pantaleev
a986e29f51 Release 1.0.4 2024-09-13 21:46:15 +03:00
Slavi Pantaleev
dd1dd78312 Rework typing notifications
Previously, the bot only had rudimentary typing notification support.

It used to send a single notification when starting a long task
and did not bother with notifications anymore.
By default matrix-rust-sdk gives these notifications a validity of 4
seconds, so it would expire shortly. If the bot takes longer to respond,
you'd see the typing notification expire and wonder if a response is
coming.

Another edge case is the bot sending an answer quicker and the typing
notice still being on. Some clients (like element-web) seem to hide the
typing notice when a new message comes, so they don't experience this as
problematic.

The reworked typing notification system should be robust:

- typing notices are sent continuously, until the bot finishes doing
  work
- if the bot is performing multiple actions in a room (even for
  different people), typing notices would continue to be sent until the
  bot becomes idle
- as soon as the bot becomes idle, a "not typing anymore" notice is sent
  to clear the state
2024-09-13 21:40:24 +03:00
Slavi Pantaleev
f2b1115dc9 Populate CHANGELOG 2024-09-13 21:39:19 +03:00
Slavi Pantaleev
5742d88d45 Release 1.0.3 2024-09-13 12:33:35 +03:00
Slavi Pantaleev
1be035d94c Upgrade mxlink (1.0.0 -> 1.1.0)
This brings in a fix that allows for auto-recovery from errors
that occur during matrix-rust-sdk startup.
2024-09-13 11:38:07 +03:00
Slavi Pantaleev
601420d561 Fix broken links to "sample provider configs"
[skip ci]
2024-09-12 22:18:51 +03:00
Slavi Pantaleev
b18be4e452 Decrease number of keywords in Cargo.toml to the maximum allowed (5) 2024-09-12 22:03:40 +03:00
Slavi Pantaleev
d1622abd47 Switch from exclude to include in Cargo.toml to whitelist the logo 2024-09-12 22:00:08 +03:00
Slavi Pantaleev
62d88660e9 Bump version (1.0.0 -> 1.0.1) 2024-09-12 21:52:17 +03:00
Slavi Pantaleev
adc129505c Switch from openai_api_rust via git repository to published etke_openai_api_rust crate 2024-09-12 21:51:13 +03:00
Slavi Pantaleev
cc28935ab1 Add .github to Cargo.toml exclude list
[skip ci]
2024-09-12 21:16:40 +03:00
Slavi Pantaleev
e09f26ce0d Fix broken link to Handlers docs page
[skip ci]
2024-09-12 16:34:42 +03:00
Slavi Pantaleev
d6349b180f Add Handlers link to docs README page
[skip ci]
2024-09-12 16:34:10 +03:00
Slavi Pantaleev
a9580bae6b Minor docs fixups
[skip ci]
2024-09-12 16:16:28 +03:00
Slavi Pantaleev
4a3d2082cb Fix some incorrect commands in the documentation
[skip ci]
2024-09-12 14:44:09 +03:00
Slavi Pantaleev
a00e738434 Fix wording
[skip ci]
2024-09-12 14:37:56 +03:00
32 changed files with 297 additions and 206 deletions

View File

@@ -1 +1,26 @@
There's nothing here yet. # (2024-09-19) Version 1.0.6
Improvements to:
- messages sent by the bot - better onboarding flow, especially when no agents have been created yet
- documentation pages
# (2024-09-14) Version 1.0.5
Further [improves](https://github.com/etkecc/baibot/commit/3b25b92a81a05ebaf1c6dbabf675fbfbe6c9f418) the typing notification logic, so that it tolerates edge cases better.
# (2024-09-14) Version 1.0.4
[Improves](https://github.com/etkecc/baibot/commit/dd1dd78312e3db7f92b37fb3b4750fbe35de7115) the typing notification logic.
# (2024-09-13) Version 1.0.3
Contains [fixes](https://github.com/etkecc/rust-mxlink/commit/f339fc85e69aa7f614394ad303d1614cd307319c) for [some](https://github.com/etkecc/baibot/issues/1) startup failures caused by partial initialization (errors during startup).
# (2024-09-12) Version 1.0.0
Initial release. 🎉

35
Cargo.lock generated
View File

@@ -297,16 +297,16 @@ dependencies = [
[[package]] [[package]]
name = "baibot" name = "baibot"
version = "1.0.0" version = "1.0.6"
dependencies = [ dependencies = [
"anthropic-rs", "anthropic-rs",
"anyhow", "anyhow",
"async-openai 0.24.0", "async-openai 0.24.0",
"base64 0.22.1", "base64 0.22.1",
"etke_openai_api_rust",
"matrix-sdk", "matrix-sdk",
"mxidwc", "mxidwc",
"mxlink", "mxlink",
"openai_api_rust",
"quick_cache", "quick_cache",
"regex", "regex",
"serde", "serde",
@@ -919,6 +919,20 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "etke_openai_api_rust"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1499ac61bf9b9a5973a5830894cbf2016a96a4dab9eb2dab4a526c720632c93"
dependencies = [
"log",
"mime",
"rand",
"serde",
"serde_json",
"ureq",
]
[[package]] [[package]]
name = "event-listener" name = "event-listener"
version = "4.0.3" version = "4.0.3"
@@ -2108,9 +2122,9 @@ dependencies = [
[[package]] [[package]]
name = "mxlink" name = "mxlink"
version = "1.0.0" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80cb97e517e3bc8e85b4787ed87037cd324526862ca9375c6d038699010bf6c2" checksum = "f1aa513435471af3e1131dc18a1f4eb2e6fb42c11308f1be9b4f0316b3809e32"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"chacha20poly1305", "chacha20poly1305",
@@ -2204,19 +2218,6 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "openai_api_rust"
version = "0.1.9"
source = "git+https://github.com/etkecc/openai_api_rust.git?branch=next#a9183fc2e699c3d33c635c2aed3f9174f9d9cd74"
dependencies = [
"log",
"mime",
"rand",
"serde",
"serde_json",
"ureq",
]
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.66" version = "0.10.66"

View File

@@ -5,9 +5,9 @@ authors = ["Slavi Pantaleev <slavi@devture.com>"]
repository = "https://github.com/etkecc/baibot" repository = "https://github.com/etkecc/baibot"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
readme = "README.md" readme = "README.md"
keywords = ["matrix", "chat", "LLM", "openai", "anthropic", "localai"] keywords = ["matrix", "chat", "bot", "AI", "LLM"]
exclude = [".editorconfig", "justfile", "docs/", "etc/", "Dockerfile", ".dockerignore", ".gitignore"] include = ["/etc/assets/baibot-torso-768.png", "/src", "/README.md", "/CHANGELOG.md", "/LICENSE"]
version = "1.0.0" version = "1.0.6"
edition = "2021" edition = "2021"
[lib] [lib]
@@ -22,8 +22,8 @@ base64 = "0.22.*"
# We'd rather not depend on this, but we cannot use the ruma-events EventContent macro without it. # We'd rather not depend on this, but we cannot use the ruma-events EventContent macro without it.
matrix-sdk = { version = "0.7.1", default-features = false } matrix-sdk = { version = "0.7.1", default-features = false }
mxidwc = "1.0.*" mxidwc = "1.0.*"
mxlink = "1.0.*" mxlink = ">=1.2.1"
openai_api_rust = { git = "https://github.com/etkecc/openai_api_rust.git", branch = "next" } etke_openai_api_rust = "0.1.*"
quick_cache = "0.6.*" quick_cache = "0.6.*"
regex = "1.10.*" regex = "1.10.*"
serde = { version = "1.0.*", features = ["derive"], default-features = false } serde = { version = "1.0.*", features = ["derive"], default-features = false }

View File

@@ -5,7 +5,7 @@
🤖 baibot is an [AI](https://en.wikipedia.org/wiki/Artificial_intelligence) ([Large Language Model](https://en.wikipedia.org/wiki/Large_language_model)) bot for [Matrix](https://matrix.org/) built by [etke.cc](https://etke.cc/) (managed Matrix servers). 🤖 baibot is an [AI](https://en.wikipedia.org/wiki/Artificial_intelligence) ([Large Language Model](https://en.wikipedia.org/wiki/Large_language_model)) bot for [Matrix](https://matrix.org/) built by [etke.cc](https://etke.cc/) (managed Matrix servers).
The name is pronounced 'bye' and is a play on [AI](https://en.wikipedia.org/wiki/Artificial_intelligence), referencing the fictional character [🇧🇬 Bai Ganyo](https://en.wikipedia.org/wiki/Bay_Ganyo). The name is pronounced 'bye'-bot and is a play on [AI](https://en.wikipedia.org/wiki/Artificial_intelligence), referencing the fictional character [🇧🇬 Bai Ganyo](https://en.wikipedia.org/wiki/Bay_Ganyo).
It's designed as a more private and [featureful](#-features) alternative to [matrix-chatgpt-bot](https://github.com/matrixgpt/matrix-chatgpt-bot). It's designed as a more private and [featureful](#-features) alternative to [matrix-chatgpt-bot](https://github.com/matrixgpt/matrix-chatgpt-bot).
It's influenced by [chaz](https://github.com/arcuru/chaz), but does **not** use the [AIChat](https://github.com/sigoden/aichat) CLI tool and instead does everything in-process, without forking. It's influenced by [chaz](https://github.com/arcuru/chaz), but does **not** use the [AIChat](https://github.com/sigoden/aichat) CLI tool and instead does everything in-process, without forking.
@@ -19,8 +19,8 @@ It's influenced by [chaz](https://github.com/arcuru/chaz), but does **not** use
- [💬 text-generation](./docs/features.md#-text-generation): communicating with you via text - [💬 text-generation](./docs/features.md#-text-generation): communicating with you via text
- [🦻 speech-to-text](./docs/features.md#-speech-to-text): turning your voice messages into text - [🦻 speech-to-text](./docs/features.md#-speech-to-text): turning your voice messages into text
- [🗣️ text-to-speech](./docs/features.md#️-text-to-speech): turning bot or users text messages into voice messages - [🗣️ text-to-speech](./docs/features.md#%EF%B8%8F-text-to-speech): turning bot or users text messages into voice messages
- [🖌️ image-generation](./docs/features.md#-image-generation): generating images based on instructions - [🖌️ image-generation](./docs/features.md#%EF%B8%8F-image-generation): generating images based on instructions
- 🪄 Supports [seamless voice interaction](./docs/features.md#seamless-voice-interaction) (turning user voice messages into text, answering in text, then turning that text back into voice) - 🪄 Supports [seamless voice interaction](./docs/features.md#seamless-voice-interaction) (turning user voice messages into text, answering in text, then turning that text back into voice)

View File

@@ -1,9 +1,11 @@
# Table of Contents # Table of Contents
- [🔒 Access](./access.md) - [🔒 Access](./access.md)
- [🤖 Agents](./agents.md) and [☁️ Providers](./providers.md) - [🤖 Agents](./agents.md)
- [🛠️ Configuration](./configuration/README.md) - [🛠️ Configuration](./configuration/README.md)
- [🌟 Features](./features.md) - [🌟 Features](./features.md)
- [🤝 Handlers](./configuration/handlers.md)
- [☁️ Providers](./providers.md)
- [📖 Usage](./usage.md) - [📖 Usage](./usage.md)
- [🚀 Installation](./installation.md) - [🚀 Installation](./installation.md)
- [💻 Development](./development.md) - [💻 Development](./development.md)

View File

@@ -23,7 +23,7 @@ To support such use-cases, the bot has a `text-generation prefix-requirement-typ
- `command_prefix`: indicates that the bot would require that messages be prefixed with the command prefix (e.g. `!bai`) and would ignore all messages that are not prefixed - `command_prefix`: indicates that the bot would require that messages be prefixed with the command prefix (e.g. `!bai`) and would ignore all messages that are not prefixed
By default, the bot is **auto-configured (upon joining a new room)** to use the `no` setting in rooms that only include 2 users (you and the bot), and `command_prefix` in rooms with more than 2 users. To prevent surprises, the bot will **not** adjust this setting subsequently. You can manually adjust it via `!bai config room set-prefix-requirement-type VALUE`. By default, the bot is **auto-configured (upon joining a new room)** to use the `no` setting in rooms that only include 2 users (you and the bot), and `command_prefix` in rooms with more than 2 users. To prevent surprises, the bot will **not** adjust this setting subsequently. You can manually adjust it via `!bai config room text-generation set-prefix-requirement-type VALUE`.
Example: `!bai config room text-generation set-prefix-requirement-type command_prefix` (this can also be set globally, see [🛠️ Room Settings](./README.md#room-settings)) Example: `!bai config room text-generation set-prefix-requirement-type command_prefix` (this can also be set globally, see [🛠️ Room Settings](./README.md#room-settings))
@@ -51,7 +51,7 @@ The bot also supports ♻️ **context management**, which automatically adjusts
This feature relies on [tokenization](https://en.wikipedia.org/wiki/Large_language_model#Tokenization) performed by the [tiktoken-rs](https://github.com/zurawiki/tiktoken-rs) library which is [poorly well-maintained](https://github.com/zurawiki/tiktoken-rs/issues/50) and only works well for [OpenAI](../providers.md#openai) models. This feature relies on [tokenization](https://en.wikipedia.org/wiki/Large_language_model#Tokenization) performed by the [tiktoken-rs](https://github.com/zurawiki/tiktoken-rs) library which is [poorly well-maintained](https://github.com/zurawiki/tiktoken-rs/issues/50) and only works well for [OpenAI](../providers.md#openai) models.
This setting is **disabled by default**, but can be enabled via `!bai config room context-management-enabled true` (this can also be set globally, see [🛠️ Room Settings](./README.md#room-settings)). This setting is **disabled by default**, but can be enabled via `!bai config room text-generation set-context-management-enabled true` (this can also be set globally, see [🛠️ Room Settings](./README.md#room-settings)).
### ⌨️ Prompt Override ### ⌨️ Prompt Override

View File

@@ -10,8 +10,8 @@ The bot supports the following use-purposes:
- [💬 text-generation](#-text-generation): communicating with you via text - [💬 text-generation](#-text-generation): communicating with you via text
- [🦻 speech-to-text](#-speech-to-text): turning your voice messages into text - [🦻 speech-to-text](#-speech-to-text): turning your voice messages into text
- [🗣️ text-to-speech](#️-text-to-speech): turning bot or users text messages into voice messages - [🗣️ text-to-speech](#%EF%B8%8F-text-to-speech): turning bot or users text messages into voice messages
- [🖌️ image-generation](#-image-generation): generating images based on instructions - [🖌️ image-generation](#%EF%B8%8F-image-generation): generating images based on instructions
In a given room, each different purpose can be served by a different [☁️ provider](./providers.md) and model. This combination of provider and model configuration is called an [🤖 agent](./agents.md). Each purpose can be served by a different **handler** agent. In a given room, each different purpose can be served by a different [☁️ provider](./providers.md) and model. This combination of provider and model configuration is called an [🤖 agent](./agents.md). Each purpose can be served by a different **handler** agent.
@@ -32,7 +32,7 @@ A few other features (like [🗣️ Text-to-Speech](#️-text-to-speech) and [
You may also wish to see: You may also wish to see:
- [🛠️ Configuration / 💬 Text Generation](./configuration/README.md#-text-generation) for configuration options related to Text Generation - [🛠️ Configuration / 💬 Text Generation](./configuration/text-generation.md) for configuration options related to Text Generation
- [📖 Usage / 💬 Text Generation](./usage.md#-text-generation) section for more details on how to use the bot for Text Generation in a room - [📖 Usage / 💬 Text Generation](./usage.md#-text-generation) section for more details on how to use the bot for Text Generation in a room
@@ -48,7 +48,9 @@ By default, the bot **doesn't** perform text-to-speech. It can be configured for
Another use-case is to have the bot operate in [Text-to-Speech-only mode](#text-to-speech-only-mode). Another use-case is to have the bot operate in [Text-to-Speech-only mode](#text-to-speech-only-mode).
- [🛠️ Configuration / 🗣️ Text-to-Speech](./configuration/README.md#-text-to-speech) for configuration options related to Text-to-Speech You may also wish to see:
- [🛠️ Configuration / 🗣️ Text-to-Speech](./configuration/text-to-speech.md) for configuration options related to Text-to-Speech
- [📖 Usage / 🗣️ Text-to-Speech](./usage.md#-text-to-speech) section for more details on how to use the bot for Text-to-Speech in a room - [📖 Usage / 🗣️ Text-to-Speech](./usage.md#-text-to-speech) section for more details on how to use the bot for Text-to-Speech in a room
@@ -77,6 +79,10 @@ The default flow is shown in the screenshot above: your voice messages are trans
You may also configure the bot for [Seamless voice interaction](#seamless-voice-interaction) or [Transcribe-only mode](#transcribe-only-mode), etc. You may also configure the bot for [Seamless voice interaction](#seamless-voice-interaction) or [Transcribe-only mode](#transcribe-only-mode), etc.
You may also wish to see:
- [🛠️ Configuration / 🦻 Speech-to-Text](./configuration/speech-to-text.md) for configuration options related to Speech-to-Text
- [📖 Usage / 🦻 Speech-to-Text](./usage.md#-speech-to-text) section for more details on how to use the bot for Speech-to-Text in a room
#### Seamless voice interaction #### Seamless voice interaction
@@ -121,7 +127,7 @@ See a [🖼️ Screenshot of the Image Generation feature](./screenshots/image-g
You may also wish to see: You may also wish to see:
- [🛠️ Configuration / 🖌️ Image Generation](./configuration/README.md#-image-generation) for configuration options related to Image Generation - [🛠️ Configuration / 🖌️ Image Generation](./configuration/image-generation.md) for configuration options related to Image Generation
- [📖 Usage / 🖌️ Image Generation](./usage.md#-image-generation) section for more details on how to use the bot for Image Generation in a room - [📖 Usage / 🖌️ Image Generation](./usage.md#-image-generation) section for more details on how to use the bot for Image Generation in a room
- [🫵 Sticker Generation](#-sticker-generation) - a special case of Image Generation - [🫵 Sticker Generation](#-sticker-generation) - a special case of Image Generation

View File

@@ -23,17 +23,20 @@ The list of supported providers is below.
### How to choose a provider ### How to choose a provider
If you're not sure which provider to start with, we **recommend [OpenAI](#openai)** as it's the most popular and has the **widest range of capabilities**: [💬 text-generation](./features.md#-text-generation), [🖌️ image-generation](./features.md#️-image-generation), [🦻 speech-to-text](./features.md#-speech-to-text), [🗣️ text-to-speech](./features.md#️-text-to-speech). If you're not sure which provider to start with, **we recommend [OpenAI](#openai)** as it's the most popular and has the **widest range of capabilities**: [💬 text-generation](./features.md#-text-generation), [🖌️ image-generation](./features.md#️-image-generation), [🦻 speech-to-text](./features.md#-speech-to-text), [🗣️ text-to-speech](./features.md#️-text-to-speech).
You don't need to choose just one though. The bot supports [mixing & matching models](./features.md#-mixing--matching-models), so you can use multiple providers at the same time. You don't need to choose just one though. The bot supports [mixing & matching models](./features.md#-mixing--matching-models), so you can use multiple providers at the same time.
### How to use a provider ### How to use a provider
- sign up for it 1. 📝 **Sign up for it**
- obtain an API key
- [create a new agent](./agents.md#creating-agents) 2. 🔑 **Obtain an API key**
- set it as a handler for some types of messages (see [Mixing & matching models](./features.md#-mixing--matching-models)) for a specific room or globally
3. 🤖 **Create one or more agents** in a given room or globally. Next to each provider in the [list below](#supported-providers) you'll see **🗲 Quick start** commands, but you may also refer to the [agent creation guide](./agents.md#creating-agents).
4. 🤝 **Set the new agent as a handler** for a given use-purpose like text-generation, image-generation, etc. The agent creation wizard will tell you how, but you may also refer to the [🤝 Handlers](./configuration/handlers.md) guide.
### Supported providers ### Supported providers
@@ -49,7 +52,7 @@ You don't need to choose just one though. The bot supports [mixing & matching mo
- create a room-local agent: `!bai agent create-room-local anthropic my-anthropic-agent` - create a room-local agent: `!bai agent create-room-local anthropic my-anthropic-agent`
- create a global agent: `!bai agent create-global anthropic my-anthropic-agent` - create a global agent: `!bai agent create-global anthropic my-anthropic-agent`
💡 When creating an agent, the bot will show you an up-to-date sample configuration for this provider which looks [like this](../sample-provider-configs/anthropic.yml). 💡 When creating an agent, the bot will show you an up-to-date sample configuration for this provider which looks [like this](./sample-provider-configs/anthropic.yml).
### Groq ### Groq
@@ -63,7 +66,7 @@ You don't need to choose just one though. The bot supports [mixing & matching mo
- create a room-local agent: `!bai agent create-room-local groq my-groq-agent` - create a room-local agent: `!bai agent create-room-local groq my-groq-agent`
- create a global agent: `!bai agent create-global groq my-groq-agent` - create a global agent: `!bai agent create-global groq my-groq-agent`
💡 When creating an agent, the bot will show you an up-to-date sample configuration for this provider which looks [like this](../sample-provider-configs/groq.yml). 💡 When creating an agent, the bot will show you an up-to-date sample configuration for this provider which looks [like this](./sample-provider-configs/groq.yml).
### LocalAI ### LocalAI
@@ -77,7 +80,7 @@ You don't need to choose just one though. The bot supports [mixing & matching mo
- create a room-local agent: `!bai agent create-room-local localai my-localai-agent` - create a room-local agent: `!bai agent create-room-local localai my-localai-agent`
- create a global agent: `!bai agent create-global localai my-localai-agent` - create a global agent: `!bai agent create-global localai my-localai-agent`
💡 When creating an agent, the bot will show you an up-to-date sample configuration for this provider which looks [like this](../sample-provider-configs/localai.yml). 💡 When creating an agent, the bot will show you an up-to-date sample configuration for this provider which looks [like this](./sample-provider-configs/localai.yml).
### Mistral ### Mistral
@@ -91,7 +94,7 @@ You don't need to choose just one though. The bot supports [mixing & matching mo
- create a room-local agent: `!bai agent create-room-local mistral my-mistral-agent` - create a room-local agent: `!bai agent create-room-local mistral my-mistral-agent`
- create a global agent: `!bai agent create-global mistral my-mistral-agent` - create a global agent: `!bai agent create-global mistral my-mistral-agent`
💡 When creating an agent, the bot will show you an up-to-date sample configuration for this provider which looks [like this](../sample-provider-configs/mistral.yml). 💡 When creating an agent, the bot will show you an up-to-date sample configuration for this provider which looks [like this](./sample-provider-configs/mistral.yml).
### Ollama ### Ollama
@@ -105,7 +108,7 @@ You don't need to choose just one though. The bot supports [mixing & matching mo
- create a room-local agent: `!bai agent create-room-local ollama my-ollama-agent` - create a room-local agent: `!bai agent create-room-local ollama my-ollama-agent`
- create a global agent: `!bai agent create-global ollama my-ollama-agent` - create a global agent: `!bai agent create-global ollama my-ollama-agent`
💡 When creating an agent, the bot will show you an up-to-date sample configuration for this provider which looks [like this](../sample-provider-configs/ollama.yml). 💡 When creating an agent, the bot will show you an up-to-date sample configuration for this provider which looks [like this](./sample-provider-configs/ollama.yml).
### OpenAI ### OpenAI
@@ -122,7 +125,7 @@ For services which are not fully compatible with the OpenAI API, consider using
- create a room-local agent: `!bai agent create-room-local openai my-openai-agent` - create a room-local agent: `!bai agent create-room-local openai my-openai-agent`
- create a global agent: `!bai agent create-global openai my-openai-agent` - create a global agent: `!bai agent create-global openai my-openai-agent`
💡 When creating an agent, the bot will show you an up-to-date sample configuration for this provider which looks [like this](../sample-provider-configs/openai.yml). 💡 When creating an agent, the bot will show you an up-to-date sample configuration for this provider which looks [like this](./sample-provider-configs/openai.yml).
### OpenAI Compatible ### OpenAI Compatible
@@ -139,7 +142,7 @@ This provider is just as featureful as the [OpenAI](#openai) provider, but is mo
- create a room-local agent: `!bai agent create-room-local openai-compatible my-openai-compatible-agent` - create a room-local agent: `!bai agent create-room-local openai-compatible my-openai-compatible-agent`
- create a global agent: `!bai agent create-global openai-compatible my-openai-compatible-agent` - create a global agent: `!bai agent create-global openai-compatible my-openai-compatible-agent`
💡 When creating an agent, the bot will show you an up-to-date sample configuration for this provider which looks [like this](../sample-provider-configs/openai-compatible.yml). 💡 When creating an agent, the bot will show you an up-to-date sample configuration for this provider which looks [like this](./sample-provider-configs/openai-compatible.yml).
### OpenRouter ### OpenRouter
@@ -153,7 +156,7 @@ This provider is just as featureful as the [OpenAI](#openai) provider, but is mo
- create a room-local agent: `!bai agent create-room-local openrouter my-openrouter-agent` - create a room-local agent: `!bai agent create-room-local openrouter my-openrouter-agent`
- create a global agent: `!bai agent create-global openrouter my-openrouter-agent` - create a global agent: `!bai agent create-global openrouter my-openrouter-agent`
💡 When creating an agent, the bot will show you an up-to-date sample configuration for this provider which looks [like this](../sample-provider-configs/openrouter.yml). 💡 When creating an agent, the bot will show you an up-to-date sample configuration for this provider which looks [like this](./sample-provider-configs/openrouter.yml).
### Together AI ### Together AI
@@ -167,4 +170,4 @@ This provider is just as featureful as the [OpenAI](#openai) provider, but is mo
- create a room-local agent: `!bai agent create-room-local together-ai my-together-ai-agent` - create a room-local agent: `!bai agent create-room-local together-ai my-together-ai-agent`
- create a global agent: `!bai agent create-global together-ai my-together-ai-agent` - create a global agent: `!bai agent create-global together-ai my-together-ai-agent`
💡 When creating an agent, the bot will show you an up-to-date sample configuration for this provider which looks [like this](../sample-provider-configs/together-ai.yml). 💡 When creating an agent, the bot will show you an up-to-date sample configuration for this provider which looks [like this](./sample-provider-configs/together-ai.yml).

View File

@@ -1,6 +1,5 @@
// LocalAI is based on OpenAI (async-openai), because it seems to be fully compatible. // At the time of testing, LocalAI can be powered by `openai`, but we use `openai_compat` for better reliability
// Moreover, openai_api_rust does not support speech-to-text, so if we wish to use this feature // in the event of future updates to `async-openai`.
// we need to stick to async-openai.
use super::openai_compat::Config; use super::openai_compat::Config;

View File

@@ -1,7 +1,7 @@
use openai_api_rust::audio::{AudioApi, AudioBody}; use etke_openai_api_rust::audio::{AudioApi, AudioBody};
use openai_api_rust::chat::{ChatApi, ChatBody}; use etke_openai_api_rust::chat::{ChatApi, ChatBody};
use openai_api_rust::images::{ImagesApi, ImagesBody}; use etke_openai_api_rust::images::{ImagesApi, ImagesBody};
use openai_api_rust::{Auth, Message, OpenAI}; use etke_openai_api_rust::{Auth, Message, OpenAI};
use super::super::ControllerTrait; use super::super::ControllerTrait;
use crate::agent::utils::base64_decode; use crate::agent::utils::base64_decode;

View File

@@ -1,4 +1,4 @@
use openai_api_rust::{Message, Role}; use etke_openai_api_rust::{Message, Role};
use crate::agent::provider::openai::Config as OpenAIConfig; use crate::agent::provider::openai::Config as OpenAIConfig;

View File

@@ -9,6 +9,7 @@ use mxlink::matrix_sdk::Room;
use mxlink::{ use mxlink::{
InitConfig, LoginConfig, LoginCredentials, LoginEncryption, MatrixLink, PersistenceConfig, InitConfig, LoginConfig, LoginCredentials, LoginEncryption, MatrixLink, PersistenceConfig,
TypingNoticeGuard,
}; };
use mxlink::helpers::account_data_config::{ use mxlink::helpers::account_data_config::{
@@ -210,6 +211,14 @@ impl Bot {
.await .await
} }
pub(crate) async fn start_typing_notice(&self, room: &Room) -> TypingNoticeGuard {
self.inner
.matrix_link
.rooms()
.start_typing_notice(room)
.await
}
pub async fn start(&self) -> anyhow::Result<()> { pub async fn start(&self) -> anyhow::Result<()> {
self.rooms().attach_event_handlers().await; self.rooms().attach_event_handlers().await;
self.messaging().attach_event_handlers().await; self.messaging().attach_event_handlers().await;

View File

@@ -13,7 +13,7 @@ pub async fn dispatch_controller(
match handler { match handler {
AccessControllerType::Help => {} AccessControllerType::Help => {}
_ => { _ => {
if !message_context.sender_can_manage_global_config()? { if !message_context.sender_can_manage_global_config() {
bot.messaging() bot.messaging()
.send_error_markdown_no_fail( .send_error_markdown_no_fail(
message_context.room(), message_context.room(),

View File

@@ -80,24 +80,21 @@ fn build_section_users(
message.push_str(&strings::access::users_no_patterns()); message.push_str(&strings::access::users_no_patterns());
} }
let can_manage_global_config = message_context.sender_can_manage_global_config(); if message_context.sender_can_manage_global_config() {
if let Ok(can_manage_global_config) = can_manage_global_config { message.push_str("\n\n");
if can_manage_global_config {
message.push_str("\n\n");
message.push_str(strings::the_following_commands_are_available()); message.push_str(strings::the_following_commands_are_available());
message.push('\n'); message.push('\n');
message.push_str(&strings::help::access::users_command_get(command_prefix)); message.push_str(&strings::help::access::users_command_get(command_prefix));
message.push('\n'); message.push('\n');
message.push_str(&strings::help::access::users_command_set(command_prefix)); message.push_str(&strings::help::access::users_command_set(command_prefix));
message.push_str("\n\n"); message.push_str("\n\n");
message.push_str(&strings::help::access::example_user_patterns( message.push_str(&strings::help::access::example_user_patterns(
homeserver_name, homeserver_name,
)); ));
}
} }
message message
@@ -156,27 +153,24 @@ fn build_section_room_local_agent_managers(
message.push_str(&strings::access::room_local_agent_managers_no_patterns()); message.push_str(&strings::access::room_local_agent_managers_no_patterns());
} }
let can_manage_global_config = message_context.sender_can_manage_global_config(); if message_context.sender_can_manage_global_config() {
if let Ok(can_manage_global_config) = can_manage_global_config { message.push_str("\n\n");
if can_manage_global_config { message.push_str(strings::the_following_commands_are_available());
message.push_str("\n\n"); message.push('\n');
message.push_str(strings::the_following_commands_are_available());
message.push('\n');
message.push_str( message.push_str(
&strings::help::access::room_local_agent_managers_command_get(command_prefix), &strings::help::access::room_local_agent_managers_command_get(command_prefix),
); );
message.push('\n'); message.push('\n');
message.push_str( message.push_str(
&strings::help::access::room_local_agent_managers_command_set(command_prefix), &strings::help::access::room_local_agent_managers_command_set(command_prefix),
); );
message.push_str("\n\n"); message.push_str("\n\n");
message.push_str(&strings::help::access::example_user_patterns( message.push_str(&strings::help::access::example_user_patterns(
homeserver_name, homeserver_name,
)); ));
}
} }
message message

View File

@@ -100,8 +100,6 @@ pub async fn handle_room_local(
return Ok(()); return Ok(());
}; };
message_context.room().typing_notice(true).await?;
if !try_to_ping_agent_or_complain(bot, message_context, &parsed_config.agent).await { if !try_to_ping_agent_or_complain(bot, message_context, &parsed_config.agent).await {
return Ok(()); return Ok(());
} }
@@ -140,7 +138,7 @@ pub async fn handle_global(
provider: &str, provider: &str,
agent_id_prefixless: &str, agent_id_prefixless: &str,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
if !message_context.sender_can_manage_global_config()? { if !message_context.sender_can_manage_global_config() {
bot.messaging() bot.messaging()
.send_error_markdown_no_fail( .send_error_markdown_no_fail(
message_context.room(), message_context.room(),
@@ -215,8 +213,6 @@ pub async fn handle_global(
return Ok(()); return Ok(());
}; };
message_context.room().typing_notice(true).await?;
if !try_to_ping_agent_or_complain(bot, message_context, &parsed_config.agent).await { if !try_to_ping_agent_or_complain(bot, message_context, &parsed_config.agent).await {
return Ok(()); return Ok(());
} }

View File

@@ -50,7 +50,7 @@ pub async fn handle(
.await .await
} }
PublicIdentifier::DynamicGlobal(_) => { PublicIdentifier::DynamicGlobal(_) => {
if !message_context.sender_can_manage_global_config()? { if !message_context.sender_can_manage_global_config() {
bot.messaging() bot.messaging()
.send_error_markdown_no_fail( .send_error_markdown_no_fail(
message_context.room(), message_context.room(),

View File

@@ -47,7 +47,7 @@ pub async fn handle(
} }
} }
PublicIdentifier::DynamicGlobal(_) => { PublicIdentifier::DynamicGlobal(_) => {
if !message_context.sender_can_manage_global_config()? { if !message_context.sender_can_manage_global_config() {
bot.messaging() bot.messaging()
.send_error_markdown_no_fail( .send_error_markdown_no_fail(
message_context.room(), message_context.room(),

View File

@@ -12,10 +12,7 @@ pub async fn handle(bot: &Bot, message_context: &MessageContext) -> anyhow::Resu
message.push_str(&format!("## {}", strings::help::agent::heading())); message.push_str(&format!("## {}", strings::help::agent::heading()));
message.push_str("\n\n"); message.push_str("\n\n");
message.push_str(&strings::help::agent::intro( message.push_str(&strings::help::agent::intro(bot.command_prefix()));
bot.command_prefix(),
can_manage_agents,
));
message.push('\n'); message.push('\n');
message.push_str(&strings::help::agent::intro_capabilities()); message.push_str(&strings::help::agent::intro_capabilities());
message.push_str("\n\n"); message.push_str("\n\n");
@@ -39,7 +36,7 @@ pub async fn handle(bot: &Bot, message_context: &MessageContext) -> anyhow::Resu
)); ));
message.push('\n'); message.push('\n');
if message_context.sender_can_manage_global_config()? { if message_context.sender_can_manage_global_config() {
message.push_str(&strings::help::agent::create_agent_global( message.push_str(&strings::help::agent::create_agent_global(
bot.command_prefix(), bot.command_prefix(),
)); ));

View File

@@ -40,7 +40,7 @@ async fn dispatch_config_related_handler(
bot: &Bot, bot: &Bot,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
if let SettingsStorageSource::Global = config_type { if let SettingsStorageSource::Global = config_type {
if !message_context.sender_can_manage_global_config()? { if !message_context.sender_can_manage_global_config() {
bot.messaging() bot.messaging()
.send_error_markdown_no_fail( .send_error_markdown_no_fail(
message_context.room(), message_context.room(),

View File

@@ -43,6 +43,8 @@ pub async fn handle(
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let mut original_message_is_audio = false; let mut original_message_is_audio = false;
let mut _typing_notice_guard: Option<mxlink::TypingNoticeGuard> = None;
let speech_to_text_flow_type = message_context let speech_to_text_flow_type = message_context
.room_config_context() .room_config_context()
.speech_to_text_flow_type(); .speech_to_text_flow_type();
@@ -58,11 +60,11 @@ pub async fn handle(
return Ok(()); return Ok(());
} }
SpeechToTextFlowType::TranscribeAndGenerateText => { SpeechToTextFlowType::TranscribeAndGenerateText => {
tracing::debug!("Will be trascribing and possibly generating text.."); tracing::debug!("Will be transcribing and possibly generating text..");
MessageResponseType::InThread(message_context.thread_info().clone()) MessageResponseType::InThread(message_context.thread_info().clone())
} }
SpeechToTextFlowType::OnlyTranscribe => { SpeechToTextFlowType::OnlyTranscribe => {
tracing::debug!("Will only be trascribing audio to text.."); tracing::debug!("Will only be transcribing audio to text..");
if message_context.thread_info().is_thread_root_only() { if message_context.thread_info().is_thread_root_only() {
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()) MessageResponseType::Reply(message_context.thread_info().root_event_id.clone())
} else { } else {
@@ -71,6 +73,10 @@ pub async fn handle(
} }
}; };
if _typing_notice_guard.is_none() {
_typing_notice_guard = Some(bot.start_typing_notice(message_context.room()).await);
}
let Some(speech_to_text_created_event_id_result) = let Some(speech_to_text_created_event_id_result) =
handle_stage_speech_to_text(bot, message_context, audio_content, response_type).await handle_stage_speech_to_text(bot, message_context, audio_content, response_type).await
else { else {
@@ -96,6 +102,10 @@ pub async fn handle(
.room_config_context() .room_config_context()
.should_auto_text_generate(original_message_is_audio) .should_auto_text_generate(original_message_is_audio)
{ {
if _typing_notice_guard.is_none() {
_typing_notice_guard = Some(bot.start_typing_notice(message_context.room()).await);
}
let speech_to_text_created_event_id_reaction_event_id = let speech_to_text_created_event_id_reaction_event_id =
if let Some(speech_to_text_created_event_id) = speech_to_text_created_event_id { if let Some(speech_to_text_created_event_id) = speech_to_text_created_event_id {
let reaction_event_response = bot let reaction_event_response = bot
@@ -213,6 +223,10 @@ pub async fn handle(
match text_to_speech_stage_params { match text_to_speech_stage_params {
Some(TextToSpeechParams::Perform(text_to_speech_eligible_payload, response_type)) => { Some(TextToSpeechParams::Perform(text_to_speech_eligible_payload, response_type)) => {
if _typing_notice_guard.is_none() {
_typing_notice_guard = Some(bot.start_typing_notice(message_context.room()).await);
}
let _tts_result = generate_and_send_tts_for_message( let _tts_result = generate_and_send_tts_for_message(
bot, bot,
matrix_link.clone(), matrix_link.clone(),
@@ -337,8 +351,6 @@ async fn handle_stage_text_generation(
) )
.await?; .await?;
_ = message_context.room().typing_notice(true).await;
let prefixes_to_strip = match controller_type { let prefixes_to_strip = match controller_type {
ChatCompletionControllerType::ViaText { prefixes_to_strip } => prefixes_to_strip.clone(), ChatCompletionControllerType::ViaText { prefixes_to_strip } => prefixes_to_strip.clone(),
ChatCompletionControllerType::ViaAudio => vec![], ChatCompletionControllerType::ViaAudio => vec![],
@@ -498,8 +510,6 @@ async fn handle_stage_speech_to_text_actual_transcribing(
.get_media_content(&media_request, true) .get_media_content(&media_request, true)
.await?; .await?;
_ = message_context.room().typing_notice(true).await;
let span = tracing::debug_span!( let span = tracing::debug_span!(
"speech_to_text_generation", "speech_to_text_generation",
agent_id = agent.identifier().as_string() agent_id = agent.identifier().as_string()

View File

@@ -3,7 +3,7 @@ use mxlink::MessageResponseType;
use crate::{entity::MessageContext, strings, Bot}; use crate::{entity::MessageContext, strings, Bot};
pub async fn handle(bot: &Bot, message_context: &MessageContext) -> anyhow::Result<()> { pub async fn handle(bot: &Bot, message_context: &MessageContext) -> anyhow::Result<()> {
let sender_can_manage_global_config = message_context.sender_can_manage_global_config()?; let sender_can_manage_global_config = message_context.sender_can_manage_global_config();
let sender_can_manage_room_local_agents = let sender_can_manage_room_local_agents =
message_context.sender_can_manage_room_local_agents()?; message_context.sender_can_manage_room_local_agents()?;
@@ -18,10 +18,7 @@ pub async fn handle(bot: &Bot, message_context: &MessageContext) -> anyhow::Resu
// Agents // Agents
message.push_str(&format!("## {}", strings::help::agent::heading())); message.push_str(&format!("## {}", strings::help::agent::heading()));
message.push_str("\n\n"); message.push_str("\n\n");
message.push_str(&strings::help::agent::intro( message.push_str(&strings::help::agent::intro(bot.command_prefix()));
bot.command_prefix(),
sender_can_manage_room_local_agents,
));
message.push_str("\n\n"); message.push_str("\n\n");
message.push_str(&strings::help::agent::intro_handler_relation( message.push_str(&strings::help::agent::intro_handler_relation(
bot.command_prefix(), bot.command_prefix(),

View File

@@ -56,8 +56,6 @@ pub async fn handle_image(
original_prompt.to_owned() original_prompt.to_owned()
}; };
message_context.room().typing_notice(true).await?;
let span = tracing::debug_span!( let span = tracing::debug_span!(
"image_generation", "image_generation",
agent_id = agent.identifier().as_string() agent_id = agent.identifier().as_string()
@@ -139,7 +137,7 @@ pub async fn handle_sticker(
return Ok(()); return Ok(());
}; };
message_context.room().typing_notice(true).await?; let _typing_notice_guard = bot.start_typing_notice(message_context.room()).await;
let span = tracing::debug_span!( let span = tracing::debug_span!(
"sticker_generation", "sticker_generation",

View File

@@ -9,17 +9,8 @@ pub fn determine_controller(_text: &str) -> ControllerType {
} }
pub async fn handle_help(message_context: &MessageContext, bot: &Bot) -> anyhow::Result<()> { pub async fn handle_help(message_context: &MessageContext, bot: &Bot) -> anyhow::Result<()> {
if !message_context.sender_can_manage_room_local_agents()? { let can_create_global_agents = message_context.sender_can_manage_global_config();
bot.messaging() let can_create_room_local_agents = message_context.sender_can_manage_room_local_agents()?;
.send_error_markdown_no_fail(
message_context.room(),
&strings::provider::not_allowed(),
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()),
)
.await;
return Ok(());
}
let mut message = String::new(); let mut message = String::new();
message.push_str(&format!("## {}", strings::help::provider::heading())); message.push_str(&format!("## {}", strings::help::provider::heading()));
@@ -69,18 +60,28 @@ pub async fn handle_help(message_context: &MessageContext, bot: &Bot) -> anyhow:
&provider_info, &provider_info,
)); ));
message.push_str("- 🗲 Quick start:\n"); // We always show a "Quick start" section (even to unprivileged users),
message.push_str(&format!( // because we're talking about it in a previous message.
"\t- create a room-local agent: `{command_prefix} agent create-room-local {provider_id} my-{provider_id}-agent`", message.push_str("- 🗲 Quick start:");
command_prefix = bot.command_prefix(), if can_create_room_local_agents {
provider_id = provider.to_static_str(), message.push_str(&format!(
)); "\n\t- create a room-local agent: `{command_prefix} agent create-room-local {provider_id} my-{provider_id}-agent`",
message.push('\n'); command_prefix = bot.command_prefix(),
message.push_str(&format!( provider_id = provider.to_static_str(),
"\t- create a global agent: `{command_prefix} agent create-global {provider_id} my-{provider_id}-agent`", ));
command_prefix = bot.command_prefix(), }
provider_id = provider.to_static_str(), if can_create_global_agents {
)); message.push_str(&format!(
"\n\t- create a global agent: `{command_prefix} agent create-global {provider_id} my-{provider_id}-agent`",
command_prefix = bot.command_prefix(),
provider_id = provider.to_static_str(),
));
}
if !can_create_room_local_agents && !can_create_global_agents {
message.push_str(&format!(
" ask an administrator to create an agent for you (you lack permissions to do so yourself)",
));
}
message.push_str("\n\n"); message.push_str("\n\n");
} }

View File

@@ -52,6 +52,8 @@ pub(super) async fn handle(
return Ok(()); return Ok(());
}; };
let _typing_notice_guard = bot.start_typing_notice(message_context.room()).await;
crate::controller::utils::text_to_speech::generate_and_send_tts_for_message( crate::controller::utils::text_to_speech::generate_and_send_tts_for_message(
bot, bot,
matrix_link, matrix_link,

View File

@@ -18,8 +18,6 @@ pub async fn generate_and_send_tts_for_message(
text_message_event_id: &OwnedEventId, text_message_event_id: &OwnedEventId,
text_content: &str, text_content: &str,
) -> bool { ) -> bool {
_ = message_context.room().typing_notice(true).await;
let reaction_event_response = bot let reaction_event_response = bot
.reacting() .reacting()
.react_no_fail( .react_no_fail(

View File

@@ -70,12 +70,12 @@ impl MessageContext {
&self.thread_info &self.thread_info
} }
pub fn sender_can_manage_global_config(&self) -> anyhow::Result<bool> { pub fn sender_can_manage_global_config(&self) -> bool {
Ok(self.trigger_event_info.sender_is_admin) self.trigger_event_info.sender_is_admin
} }
pub fn sender_can_manage_room_local_agents(&self) -> anyhow::Result<bool> { pub fn sender_can_manage_room_local_agents(&self) -> mxidwc::Result<bool> {
Ok(self.sender_can_manage_global_config()? Ok(self.sender_can_manage_global_config()
|| self.sender_is_allowed_room_local_agent_manager()?) || self.sender_is_allowed_room_local_agent_manager()?)
} }
@@ -102,21 +102,8 @@ impl MessageContext {
combined combined
} }
fn sender_is_allowed_room_local_agent_manager(&self) -> anyhow::Result<bool> { fn sender_is_allowed_room_local_agent_manager(&self) -> mxidwc::Result<bool> {
match &self self.room_config_context()
.global_config() .is_user_allowed_room_local_agent_manager(self.sender_id().clone())
.access
.room_local_agent_manager_patterns
{
None => Ok(false),
Some(patterns) => {
let allowed_regexes = mxidwc::parse_patterns_vector(patterns)?;
Ok(mxidwc::match_user_id(
self.sender_id().as_str(),
&allowed_regexes,
))
}
}
} }
} }

View File

@@ -1,3 +1,5 @@
use mxlink::matrix_sdk::ruma::OwnedUserId;
use super::globalconfig::GlobalConfig; use super::globalconfig::GlobalConfig;
use super::roomconfig::RoomConfig; use super::roomconfig::RoomConfig;
@@ -180,4 +182,18 @@ impl RoomConfigContext {
.clone() .clone()
}) })
} }
pub fn is_user_allowed_room_local_agent_manager(
&self,
user_id: OwnedUserId,
) -> mxidwc::Result<bool> {
match &self.global_config.access.room_local_agent_manager_patterns {
None => Ok(false),
Some(patterns) => {
let allowed_regexes = mxidwc::parse_patterns_vector(patterns)?;
Ok(mxidwc::match_user_id(user_id.as_str(), &allowed_regexes))
}
}
}
} }

View File

@@ -44,7 +44,7 @@ pub fn not_allowed_to_manage_static_agents() -> String {
pub fn configuration_does_not_result_in_a_working_agent(err: anyhow::Error) -> String { pub fn configuration_does_not_result_in_a_working_agent(err: anyhow::Error) -> String {
format!( format!(
"The provided configuration does not result in a working agent. The following error was encountered when trying to talk to the agent API:\n```\n{}```", "The provided configuration does not result in a working agent. The following error was encountered when trying to talk to the agent API:\n```\n{}\n```",
err, err,
) )
} }

View File

@@ -2,15 +2,8 @@ pub fn heading() -> String {
"🤖 Agents".to_owned() "🤖 Agents".to_owned()
} }
pub fn intro(command_prefix: &str, can_see_providers: bool) -> String { pub fn intro(command_prefix: &str) -> String {
format!( format!("An agent is an instantiation and configuration of some **☁️ provider** (see `{command_prefix} provider`).")
"An agent is an instantiation and configuration of some **☁️ provider**{}.",
if can_see_providers {
format!(" (see `{command_prefix} provider`)")
} else {
"".to_owned()
}
)
} }
pub fn intro_handler_relation(command_prefix: &str) -> String { pub fn intro_handler_relation(command_prefix: &str) -> String {
@@ -24,7 +17,7 @@ pub fn intro_capabilities() -> String {
} }
pub fn no_permission_to_create_agents() -> &'static str { pub fn no_permission_to_create_agents() -> &'static str {
"You are neither an administrator, nor a room-local agent manager, so **you cannot create new agents by yourself**." "⚠️ You are neither a bot administrator, nor a room-local agent manager, so **you cannot create new agents by yourself**."
} }
pub fn list_agents(command_prefix: &str) -> String { pub fn list_agents(command_prefix: &str) -> String {

View File

@@ -23,20 +23,6 @@ fn purposes_intro() -> &'static str {
"I can typically be used for the following purposes:" "I can typically be used for the following purposes:"
} }
fn no_text_generation_handler_agent() -> String {
format!(
"There is no configured handler agent which supports {} {}.",
AgentPurpose::TextGeneration.emoji(),
AgentPurpose::TextGeneration
)
}
fn introduction_outro(command_prefix: &str) -> String {
format!(
"You may also send a `{command_prefix} help` command message in this room for more information."
)
}
pub async fn create_on_join_introduction( pub async fn create_on_join_introduction(
name: &str, name: &str,
command_prefix: &str, command_prefix: &str,
@@ -111,18 +97,17 @@ pub async fn create_on_join_introduction(
message.push_str("\n\n"); message.push_str("\n\n");
if got_text_generation_agent { if got_text_generation_agent {
message.push_str(&simply_send_a_message( message.push_str(&make_use_of_me_simply_send_a_message(
command_prefix, command_prefix,
room_config_context.text_generation_prefix_requirement_type(), room_config_context.text_generation_prefix_requirement_type(),
)); ));
} else { } else {
message.push_str(&no_text_generation_handler_agent()); message.push_str(&make_use_of_me_agent_creation(
command_prefix,
room_config_context.text_generation_prefix_requirement_type(),
));
} }
message.push_str("\n\n");
message.push_str(&introduction_outro(command_prefix));
message message
} }
@@ -130,17 +115,85 @@ pub fn create_short_introduction(name: &str) -> String {
its_me(name) its_me(name)
} }
fn simply_send_a_message( fn make_use_of_me_simply_send_a_message(
command_prefix: &str,
prefix_requirement_type: TextGenerationPrefixRequirementType,
) -> String {
let message = r#"**To make use of me**:
1. 👋 %send_a_message%
2. 📖 %learn_more%
"#;
message
.replace("%command_prefix%", command_prefix)
.replace(
"%send_a_message%",
&send_a_text_message(command_prefix, prefix_requirement_type),
)
.replace(
"%learn_more%",
&learn_more_from_usage_or_help(command_prefix),
)
}
fn make_use_of_me_agent_creation(
command_prefix: &str,
prefix_requirement_type: TextGenerationPrefixRequirementType,
) -> String {
let message = r#"**To make use of me**:
1. ☁️ **Choose an agent provider** (e.g. OpenAI, Mistral, etc). Send a `%command_prefix% provider` command to see the list.
2. 🤖 %create_one_or_more_agents%
3. 🤝 %set_new_agent_as_handler%
4. 👋 %send_a_message%
5. 📖 %learn_more%
"#;
message
.replace("%command_prefix%", command_prefix)
.replace(
"%send_a_message%",
&send_a_text_message(command_prefix, prefix_requirement_type),
)
.replace(
"%learn_more%",
&learn_more_from_usage_or_help(command_prefix),
)
.replace(
"%create_one_or_more_agents%",
&create_one_or_more_agents(command_prefix),
)
.replace(
"%set_new_agent_as_handler%",
&set_new_agent_as_handler(command_prefix),
)
}
fn send_a_text_message(
command_prefix: &str, command_prefix: &str,
prefix_requirement_type: TextGenerationPrefixRequirementType, prefix_requirement_type: TextGenerationPrefixRequirementType,
) -> String { ) -> String {
match prefix_requirement_type { match prefix_requirement_type {
TextGenerationPrefixRequirementType::No => { TextGenerationPrefixRequirementType::No => {
"**To make use of me, send a message** in this room (e.g. `Hello!`) and see me reply." "**Send a text message** in this room (e.g. `Hello!`) and see me reply.".to_owned()
.to_owned()
} }
TextGenerationPrefixRequirementType::CommandPrefix => { TextGenerationPrefixRequirementType::CommandPrefix => {
format!("In this room, I'm configured to require the command prefix (`{command_prefix}`) for text messages.\n**To make use of me, send a prefixed text message** (e.g. `{command_prefix} Hello!`) and see me reply.") format!("In this room, I'm configured to require the command prefix (`{command_prefix}`) for text messages. **Send a prefixed text message** (e.g. `{command_prefix} Hello!`) and see me reply.")
} }
} }
} }
fn learn_more_from_usage_or_help(command_prefix: &str) -> String {
format!(
"**Learn more** by sending a `{command_prefix} usage` or `{command_prefix} help` command."
)
}
pub fn create_one_or_more_agents(command_prefix: &str) -> String {
format!("**Create one or more agents** in this room or globally. The provider help message will show you **🗲 Quick start** commands, but you may also send a `{command_prefix} agent` command to see the guide.")
}
pub fn set_new_agent_as_handler(command_prefix: &str) -> String {
format!("**Set the new agent as a handler** for a given use-purpose like text-generation, image-generation, etc. The agent-creation wizard will tell you how, but you may also send a `{command_prefix} config` command to see the guide (in the 🤖 *Handler Agents* section).")
}

View File

@@ -25,10 +25,6 @@ pub fn invalid_configuration_for_provider(
) )
} }
pub fn not_allowed() -> String {
"You are not allowed to see the providers list.".to_owned()
}
pub fn providers_list_intro() -> String { pub fn providers_list_intro() -> String {
"The list of supported providers is below.".to_owned() "The list of supported providers is below.".to_owned()
} }
@@ -39,7 +35,7 @@ pub fn help_how_to_choose_heading() -> String {
pub fn help_how_to_choose_description(command_prefix: &str) -> String { pub fn help_how_to_choose_description(command_prefix: &str) -> String {
let str = r#" let str = r#"
If you're not sure which provider to start with, we **recommend OpenAI** as it's the most popular and has the **widest range of capabilities**. If you're not sure which provider to start with, **we recommend OpenAI** as it's the most popular and has the **widest range of capabilities**.
You don't need to choose just one though. The bot supports **mixing & matching models** (by setting different handlers for different types of messages - see `%command_prefix% config`), so you can use multiple providers at the same time. You don't need to choose just one though. The bot supports **mixing & matching models** (by setting different handlers for different types of messages - see `%command_prefix% config`), so you can use multiple providers at the same time.
"#; "#;
@@ -55,13 +51,21 @@ pub fn help_how_to_use_heading() -> String {
pub fn help_how_to_use_description(command_prefix: &str) -> String { pub fn help_how_to_use_description(command_prefix: &str) -> String {
let str = r#" let str = r#"
- sign up for it 1. 📝 **Sign up for it**
- obtain an API key 2. 🔑 **Obtain an API key**
- create a new agent (see `%command_prefix% agent`) 3. 🤖 %create_one_or_more_agents%
- set the new agent as a handler for some types of messages (see `%command_prefix% config`) 4. 🤝 %set_new_agent_as_handler%
"#; "#;
str.replace("%command_prefix%", command_prefix) str.replace("%command_prefix%", command_prefix)
.replace(
"%create_one_or_more_agents%",
&super::introduction::create_one_or_more_agents(command_prefix),
)
.replace(
"%set_new_agent_as_handler%",
&super::introduction::set_new_agent_as_handler(command_prefix),
)
.trim() .trim()
.to_owned() .to_owned()
} }

View File

@@ -12,7 +12,7 @@ If there's a text-generation handler agent configured (see `%command_prefix% con
Whether the bot responds depends on the **💬 Text Generation / 🗟 Prefix Requirement** setting (see `%command_prefix% config status`). Whether the bot responds depends on the **💬 Text Generation / 🗟 Prefix Requirement** setting (see `%command_prefix% config status`).
Sometimes, a prefix (e.g. `%command_prefix%`) is required in front of messages sent to the room for the bot to respond. Sometimes, a prefix (e.g. `%command_prefix%`) is required in front of messages sent to the room for the bot to respond.
For multi-user rooms, this setting defaults to "required" For multi-user rooms, this setting defaults to "required".
Room messages start a threaded conversation where you can continue back-and-forth communication with the bot. Room messages start a threaded conversation where you can continue back-and-forth communication with the bot.