Compare commits
7 Commits
v1.19.3
...
access-tok
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2eb7e94bc | ||
|
|
952b75318e | ||
|
|
ce42942343 | ||
|
|
9a226af36f | ||
|
|
0048226dc4 | ||
|
|
0361f9a100 | ||
|
|
1d8f2b6890 |
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: CI
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
tags: [ "v*" ]
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
concurrency:
|
||||
group: ci-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
test-and-clippy:
|
||||
name: Unit testing and linting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@1.93.0
|
||||
- name: Install SQLite3
|
||||
run: sudo apt-get update && sudo apt-get install -y libsqlite3-dev
|
||||
- run: cargo test --all-features
|
||||
- run: cargo clippy
|
||||
124
.github/workflows/publish.yml
vendored
124
.github/workflows/publish.yml
vendored
@@ -1,124 +0,0 @@
|
||||
name: Publish
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "CI" ]
|
||||
types: [ "completed" ]
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: publish-${{ github.event.workflow_run.id || github.ref }}
|
||||
cancel-in-progress: false
|
||||
jobs:
|
||||
docker-clean-metadata:
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
(
|
||||
github.event.workflow_run.head_branch == 'main' ||
|
||||
startsWith(github.event.workflow_run.head_branch || '', 'v')
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
json: ${{ steps.meta.outputs.json }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
fetch-depth: 0
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event.workflow_run.head_branch == 'main' }}
|
||||
type=semver,pattern={{raw}},value=${{ github.event.workflow_run.head_branch }},enable=${{ startsWith(github.event.workflow_run.head_branch || '', 'v') }}
|
||||
|
||||
docker-build:
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
(
|
||||
github.event.workflow_run.head_branch == 'main' ||
|
||||
startsWith(github.event.workflow_run.head_branch || '', 'v')
|
||||
)
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: self-hosted
|
||||
arch: amd64
|
||||
- os: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
fetch-depth: 0
|
||||
- name: Log in to the GitHub Container registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event.workflow_run.head_branch == 'main' }}
|
||||
type=semver,pattern={{raw}},value=${{ github.event.workflow_run.head_branch }},enable=${{ startsWith(github.event.workflow_run.head_branch || '', 'v') }}
|
||||
flavor: |
|
||||
latest=auto
|
||||
suffix=-${{ matrix.arch }},onlatest=true
|
||||
images: |
|
||||
ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
docker-manifest:
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
(
|
||||
github.event.workflow_run.head_branch == 'main' ||
|
||||
startsWith(github.event.workflow_run.head_branch || '', 'v')
|
||||
)
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
needs:
|
||||
- docker-build
|
||||
- docker-clean-metadata
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
image: ${{ fromJson(needs.docker-clean-metadata.outputs.json).tags }}
|
||||
|
||||
steps:
|
||||
- name: Log in to the GitHub Container registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create and push manifest
|
||||
run: |
|
||||
docker buildx imagetools create -t ${{ matrix.image }} ${{ matrix.image }}-amd64 ${{ matrix.image }}-arm64
|
||||
107
.github/workflows/workflow.yml
vendored
Normal file
107
.github/workflows/workflow.yml
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
name: CI (main and tags)
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
tags: [ "v*" ]
|
||||
permissions:
|
||||
checks: write
|
||||
contents: write
|
||||
packages: write
|
||||
pull-requests: read
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
jobs:
|
||||
test-and-clippy:
|
||||
name: Unit testing and linting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install SQLite3
|
||||
run: sudo apt-get update && sudo apt-get install -y libsqlite3-dev
|
||||
- run: cargo test --all-features
|
||||
- run: cargo clippy
|
||||
|
||||
docker-clean-metadata:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
json: ${{ steps.meta.outputs.json }}
|
||||
steps:
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.ref_name == 'main' }}
|
||||
type=semver,pattern={{raw}}
|
||||
|
||||
docker-build:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: self-hosted
|
||||
arch: amd64
|
||||
- os: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- name: Log in to the GitHub Container registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.ref_name == 'main' }}
|
||||
type=semver,pattern={{raw}}
|
||||
flavor: |
|
||||
latest=auto
|
||||
suffix=-${{ matrix.arch }},onlatest=true
|
||||
images: |
|
||||
ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
docker-manifest:
|
||||
needs:
|
||||
- docker-build
|
||||
- docker-clean-metadata
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
image: ${{ fromJson(needs.docker-clean-metadata.outputs.json).tags }}
|
||||
|
||||
steps:
|
||||
- name: Log in to the GitHub Container registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create and push manifest
|
||||
run: |
|
||||
docker manifest create ${{ matrix.image }} ${{ matrix.image }}-amd64 ${{ matrix.image }}-arm64
|
||||
docker manifest push ${{ matrix.image }}
|
||||
70
CHANGELOG.md
70
CHANGELOG.md
@@ -1,73 +1,3 @@
|
||||
# (2026-05-27) Version 1.19.3
|
||||
|
||||
- (**Internal Improvement**) Update [async-openai](https://crates.io/crates/async-openai) to 0.40.2, pulling in several upstream fixes (streaming HTTP error surfacing, default `ResponseTextParam.format` deserialization, etc.).
|
||||
|
||||
- (**Internal Improvement**) Dependency updates.
|
||||
|
||||
|
||||
# (2026-05-21) Version 1.19.2
|
||||
|
||||
- (**Internal Improvement**) Update [async-openai](https://crates.io/crates/async-openai) to 0.40.0.
|
||||
|
||||
- (**Internal Improvement**) Dependency updates.
|
||||
|
||||
|
||||
# (2026-05-09) Version 1.19.1
|
||||
|
||||
- (**Internal Improvement**) Update [async-openai](https://crates.io/crates/async-openai) to 0.38.0.
|
||||
|
||||
- (**Internal Improvement**) Dependency updates.
|
||||
|
||||
|
||||
# (2026-05-09) Version 1.19.0
|
||||
|
||||
- (**Internal Improvement**) Update [matrix-sdk](https://crates.io/crates/matrix-sdk) from 0.16 to 0.17 and [mxlink](https://crates.io/crates/mxlink) to 1.14.0. matrix-sdk 0.17 dropped its `native-tls` feature and now uses [rustls](https://github.com/rustls/rustls) exclusively as its TLS backend.
|
||||
|
||||
- (**Internal Improvement**) Bump the pinned Rust toolchain from 1.93.0 to 1.95.0 (in `rust-toolchain.toml` and the Docker build images).
|
||||
|
||||
- (**Internal Improvement**) Dependency updates.
|
||||
|
||||
|
||||
# (2026-04-11) Version 1.18.0
|
||||
|
||||
- (**Bugfix**) Fix the bot not sending a welcome message when joining a room on homeservers (like [Continuwuity](https://continuwuity.org/)) that place the join membership event in the sync response's `state` block rather than the `timeline` block, via [mxlink](https://crates.io/crates/mxlink) 1.13.1
|
||||
|
||||
- (**Improvement**) Update [tiktoken-rs](https://crates.io/crates/tiktoken-rs) to 0.11, adding tokenization support for newer GPT models (gpt-5.x, codex, etc.) and fixing context sizes for o1-mini/chatgpt-4o/gpt-4.5
|
||||
|
||||
- (**Internal Improvement**) Dependency updates
|
||||
|
||||
|
||||
# (2026-03-25) Version 1.17.0
|
||||
|
||||
- (**Feature**) Add `text-generation sender-context-mode` for attaching sender metadata to conversation messages. See the [💬 Text Generation](./docs/configuration/text-generation.md#-sender-context-mode) documentation for details. Thanks to [kschwank](https://github.com/kschwank) for the contribution in [#104](https://github.com/etkecc/baibot/pull/104)!
|
||||
|
||||
|
||||
# (2026-03-24) Version 1.16.1
|
||||
|
||||
- (**Bugfix**) Fix compatibility with [async-openai](https://crates.io/crates/async-openai) 0.34.0 by populating the new `phase` field required for OpenAI Responses API message inputs. baibot does not currently distinguish between assistant `commentary` and `final_answer` turns, so using `None` preserves the previous behavior while remaining compatible with the updated crate.
|
||||
|
||||
- (**Internal Improvement**) Dependency updates.
|
||||
|
||||
|
||||
# (2026-03-20) Version 1.16.0
|
||||
|
||||
- (**Feature**) Add support for file attachments (`m.file` Matrix messages) in conversations. Files like PDFs, text documents, spreadsheets, code files, etc. are now downloaded and forwarded to the LLM alongside the conversation context, similar to how images (`m.image`) are already handled. See the [💬 Text Generation](./docs/features.md#-text-generation) documentation for details and known limitations.
|
||||
|
||||
- (**Improvement**) Use the [mime_guess](https://crates.io/crates/mime_guess) crate for MIME type detection from file extensions, replacing a hand-maintained mapping. This covers hundreds of file extensions out of the box.
|
||||
|
||||
|
||||
|
||||
# (2026-03-07) Version 1.15.0
|
||||
|
||||
- (**Feature**) Add support for authentication via access tokens (for [Matrix Authentication Service](https://github.com/element-hq/matrix-authentication-service)/OIDC-enabled homeservers) as an alternative to password authentication. See [🔐 Authentication](./docs/configuration/authentication.md) for setup details. Thanks to [Taylor Southwick](https://github.com/twsouthwick) for the contribution in [#83](https://github.com/etkecc/baibot/pull/83)!
|
||||
|
||||
- (**Internal Improvement**) Pin the Rust toolchain to `1.93.0` in both CI and local development to avoid `matrix-sdk` build failures on newer stable toolchains.
|
||||
|
||||
- (**Internal Improvement**) Documentation updates.
|
||||
|
||||
- (**Internal Improvement**) Dependency updates.
|
||||
|
||||
|
||||
# (2026-02-18) Version 1.14.3
|
||||
|
||||
- (**Internal Improvement**) Add [Renovate](https://docs.renovatebot.com/) configuration for automated dependency updates
|
||||
|
||||
1143
Cargo.lock
generated
1143
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
15
Cargo.toml
@@ -7,7 +7,7 @@ license = "AGPL-3.0-or-later"
|
||||
readme = "README.md"
|
||||
keywords = ["matrix", "chat", "bot", "AI", "LLM"]
|
||||
include = ["/etc/assets/baibot-torso-768.png", "/src", "/README.md", "/CHANGELOG.md", "/LICENSE"]
|
||||
version = "1.19.3"
|
||||
version = "1.14.3"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
@@ -17,23 +17,24 @@ path = "src/lib.rs"
|
||||
[dependencies]
|
||||
anthropic = { git = "https://github.com/etkecc/anthropic-rs.git", branch = "fix-content-block-image" }
|
||||
anyhow = "1.0.*"
|
||||
async-openai = { version = "0.40.0", features = ["audio", "chat-completion", "image", "responses"] }
|
||||
async-openai = { version = "0.33.0", features = ["audio", "chat-completion", "image", "responses"] }
|
||||
base64 = "0.22.*"
|
||||
chrono = { version = "0.4.*", default-features = false, features = ["std", "now"] }
|
||||
# We'd rather not depend on this, but we cannot use the ruma-events EventContent macro without it.
|
||||
matrix-sdk = { version = "0.17.0", default-features = false }
|
||||
# We add the `native-tls` feature, because of https://github.com/etkecc/rust-mxlink/issues/1
|
||||
matrix-sdk = { version = "0.16.0", default-features = false, features = ["native-tls"] }
|
||||
mime_guess = "2.0.*"
|
||||
mxidwc = "1.0.*"
|
||||
mxlink = ">=1.14.0"
|
||||
mxlink = ">=1.13.0"
|
||||
etke_openai_api_rust = "0.1.*"
|
||||
quick_cache = "0.6.*"
|
||||
regex = "1.12.*"
|
||||
serde = { version = "1.0.*", features = ["derive"], default-features = false }
|
||||
serde_json = "1.0.*"
|
||||
serde_yaml_ng = "0.10.*"
|
||||
tempfile = "3.27.*"
|
||||
tiktoken-rs = { version = "0.11.*", default-features = false }
|
||||
tokio = { version = "1.52.*", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
tempfile = "3.26.*"
|
||||
tiktoken-rs = { version = "0.9.*", default-features = false }
|
||||
tokio = { version = "1.50.*", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
tracing = "0.1.*"
|
||||
tracing-subscriber = { version = "0.3.*", features = ["env-filter"] }
|
||||
url = "2.5.*"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# #
|
||||
#######################################
|
||||
|
||||
FROM docker.io/rust:1.95.0-slim-trixie AS build
|
||||
FROM docker.io/rust:1.93.1-slim-trixie AS build
|
||||
|
||||
RUN apt-get update && apt-get install -y build-essential pkg-config libssl-dev libsqlite3-dev
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# #
|
||||
#######################################
|
||||
|
||||
FROM docker.io/rust:1.95.0-slim-trixie AS build
|
||||
FROM docker.io/rust:1.93.1-slim-trixie AS build
|
||||
|
||||
RUN apt-get update && apt-get install -y build-essential pkg-config libssl-dev libsqlite3-dev
|
||||
|
||||
|
||||
@@ -22,8 +22,6 @@ You can see the list of supported environment variables in the [🦀 src/entity/
|
||||
> [!WARNING]
|
||||
> The static configuration contains an `initial_global_config` key, which is used to populate the bot's global configuration (stored as [dynamic configuration](#dynamic-configuration)) the first time the bot starts. Modifying this subsequently will not have any effect. After initial global configuration creation, it's expected to be managed dynamically via chat commands.
|
||||
|
||||
For Matrix-account authentication setup, see [🔐 Authentication](./authentication.md).
|
||||
|
||||
|
||||
### Dynamic configuration
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
## 🔐 Authentication
|
||||
|
||||
baibot supports 2 authentication modes for the Matrix account (`user.*` keys in config).
|
||||
|
||||
Set **exactly one** mode. If both are set (or neither is set), startup validation fails.
|
||||
|
||||
### Password authentication
|
||||
|
||||
- Config key: `user.password`
|
||||
- Environment variable: `BAIBOT_USER_PASSWORD`
|
||||
|
||||
### Access token authentication
|
||||
|
||||
- Config keys: `user.access_token` + `user.device_id`
|
||||
- Environment variables: `BAIBOT_USER_ACCESS_TOKEN` + `BAIBOT_USER_DEVICE_ID`
|
||||
|
||||
Access-token authentication is useful for OIDC-enabled homeservers (e.g. those using [Matrix Authentication Service](https://github.com/element-hq/matrix-authentication-service)).
|
||||
|
||||
Example token-generation command:
|
||||
|
||||
```sh
|
||||
mas-cli manage issue-compatibility-token <username> [device_id]
|
||||
```
|
||||
@@ -8,7 +8,7 @@ You can also use **different models within the same room** (e.g. [💬 text-gene
|
||||
|
||||
The bot supports the following use-purposes:
|
||||
|
||||
- [💬 text-generation](../features.md#-text-generation): communicating with you via text (though certain models may also process images and files)
|
||||
- [💬 text-generation](../features.md#-text-generation): communicating with you via text (though certain models may "see" images as well)
|
||||
- [🦻 speech-to-text](../features.md#-speech-to-text): turning your voice messages into text
|
||||
- [🗣️ text-to-speech](../features.md#️-text-to-speech): turning bot or users text messages into voice messages
|
||||
- [🖌️ image-generation](../features.md#image-generation): generating images based on instructions
|
||||
|
||||
@@ -57,25 +57,6 @@ This feature relies on [tokenization](https://en.wikipedia.org/wiki/Large_langua
|
||||
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)).
|
||||
|
||||
|
||||
### 👤 Sender Context Mode
|
||||
|
||||
In multi-user rooms, it may be useful for the model to know which participant sent each message in the conversation context.
|
||||
|
||||
To support this, the bot has a `text-generation sender-context-mode` setting, which can be set to:
|
||||
|
||||
- (default) `disabled`: do not attach sender metadata to messages before sending them to the model
|
||||
|
||||
- `matrix_user_id`: prefix text messages with the sender's Matrix user ID, for example: `[sender=@alice:example.com] Hello bot`
|
||||
|
||||
- `matrix_user_id_and_timestamp`: prefix text messages with the sender's Matrix user ID and the message timestamp, for example: `[sender=@alice:example.com sent_at=2026-03-23T14:30:00Z] Hello bot`
|
||||
|
||||
This sender metadata is attached to conversation messages before they are sent to the model provider. It applies to user and assistant text messages, but not to system prompts or non-text content.
|
||||
|
||||
⚠️ Enabling this sends Matrix user IDs, and optionally timestamps, to the model provider.
|
||||
|
||||
Example: `!bai config room text-generation set-sender-context-mode matrix_user_id` (this can also be set globally, see [🛠️ Room Settings](./README.md#room-settings))
|
||||
|
||||
|
||||
### ⌨️ Prompt Override
|
||||
|
||||
You can override the [system prompt](https://huggingface.co/docs/transformers/en/tasks/prompting) configured at the [🤖 agent](../agents.md) level.
|
||||
|
||||
@@ -8,7 +8,7 @@ You can also use **different models within the same room** (e.g. [💬 text-gene
|
||||
|
||||
The bot supports the following use-purposes:
|
||||
|
||||
- [💬 text-generation](#-text-generation): communicating with you via text (though certain models may also process images and files)
|
||||
- [💬 text-generation](#-text-generation): communicating with you via text (though certain models may "see" images as well)
|
||||
- [🦻 speech-to-text](#-speech-to-text): turning your voice messages into text
|
||||
- [🗣️ text-to-speech](#%EF%B8%8F-text-to-speech): turning bot or users text messages into voice messages
|
||||
- [🖌️ image-generation](#%EF%B8%8F-image-generation): generating images based on instructions
|
||||
@@ -26,14 +26,12 @@ Text Generation is the bot's ability to **respond to users' messages with text**
|
||||
|
||||

|
||||
|
||||
Some models also support vision and document understanding, so you may be able to mix text, images, and files (PDFs, text documents, etc.) in the same conversation. Note that certain providers may not support all file types or may have issues with specific files (e.g. scanned/image-based PDFs). If a file is rejected by the provider, the conversation thread may become unusable — start a new thread to work around this.
|
||||
Some models also support vision, so you may be able to mix text and images in the same conversation.
|
||||
|
||||
In multi-user (group) rooms, to avoid disturbing the normal conversation between people, the bot is auto-configured to only respond to messages starting with the command prefix (`!bai`) or direct mentions via the [💬 Text Generation / 🗟 Prefix Requirement Type](./configuration/text-generation.md#-prefix-requirement-type) setting.
|
||||
|
||||
Normally, the bot only responds to allowed [👥 Users](./access.md#-users). In certain cases, it's useful for an allowed user to provoke the bot to respond even in foreign threads or reply chains. You can learn more about this feature in the [On-demand involvement](./features.md#on-demand-involvement) section below.
|
||||
|
||||
If needed, the bot can also attach sender metadata to conversation messages before sending them to the model, which can help the model distinguish between participants in multi-user rooms. See [🛠️ Configuration / 💬 Text Generation / 👤 Sender Context Mode](./configuration/text-generation.md#-sender-context-mode).
|
||||
|
||||
A few other features (like [🗣️ Text-to-Speech](#️-text-to-speech) and [🦻 Speech-to-Text](#-speech-to-text)) combine well with Text Generation, so you **don't necessarily need to communicate with the bot via text** (with [Seamless voice interaction](#seamless-voice-interaction), you can communicate only with voice).
|
||||
|
||||
You may also wish to see:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
base_url: https://api.openai.com/v1
|
||||
api_key: YOUR_API_KEY_HERE
|
||||
text_generation:
|
||||
model_id: gpt-5.4
|
||||
model_id: gpt-5.2
|
||||
prompt: "You are a brief, but helpful bot called {{ baibot_name }} powered by the {{ baibot_model_id }} model. The date/time of this conversation's start is: {{ baibot_conversation_start_time_utc }}."
|
||||
temperature: 1.0
|
||||
# Reasoning models need to use `max_completion_tokens` instead of `max_response_tokens`.
|
||||
|
||||
@@ -11,7 +11,7 @@ This is related to the [💬 Text Generation](./features.md#-text-generation) fe
|
||||
|
||||
If there's a text-generation handler agent configured, the bot **may** respond to messages sent in the room.
|
||||
|
||||
Some models also support vision and document understanding, so you may be able to mix text, images, and files (PDFs, text documents, etc.) in the same conversation.
|
||||
Some models also support vision, so you may be able to mix text and images in the same conversation.
|
||||
|
||||
See screenshots of:
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ user:
|
||||
# Password-based login (traditional homeservers):
|
||||
password: baibot
|
||||
|
||||
# Access token login (for Matrix Authentication Service/OIDC-enabled homeservers):
|
||||
# Access token login (for MAS/OIDC-enabled homeservers):
|
||||
# Generate a token via: mas-cli manage issue-compatibility-token <username> [device_id]
|
||||
# access_token: null
|
||||
# device_id: null
|
||||
@@ -91,7 +91,7 @@ agents:
|
||||
# base_url: https://api.openai.com/v1
|
||||
# api_key: ""
|
||||
# text_generation:
|
||||
# model_id: gpt-5.4
|
||||
# model_id: gpt-5.2
|
||||
# prompt: "You are a brief, but helpful bot called {{ baibot_name }} powered by the {{ baibot_model_id }} model. The date/time of this conversation's start is: {{ baibot_conversation_start_time_utc }}."
|
||||
# temperature: 1.0
|
||||
# # Reasoning models need to use `max_completion_tokens` instead of `max_response_tokens`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
continuwuity:
|
||||
image: forgejo.ellis.link/continuwuation/continuwuity:v0.5.9
|
||||
image: forgejo.ellis.link/continuwuation/continuwuity:v0.5.6
|
||||
user: "${UID}:${GID}"
|
||||
restart: unless-stopped
|
||||
cap_drop:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
element-web:
|
||||
image: ghcr.io/element-hq/element-web:v1.12.18
|
||||
image: ghcr.io/element-hq/element-web:v1.12.11
|
||||
user: "${UID}:${GID}"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
ollama:
|
||||
image: docker.io/ollama/ollama:0.24.0
|
||||
image: docker.io/ollama/ollama:0.17.6
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${SERVICE_OLLAMA_BIND_PORT_HTTP}:11434"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
postgres:
|
||||
image: docker.io/postgres:18.4-alpine
|
||||
image: docker.io/postgres:18.3-alpine
|
||||
user: ${UID}:${GID}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -14,7 +14,7 @@ services:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
|
||||
synapse:
|
||||
image: ghcr.io/element-hq/synapse:v1.153.0
|
||||
image: ghcr.io/element-hq/synapse:v1.148.0
|
||||
user: "${UID}:${GID}"
|
||||
restart: unless-stopped
|
||||
entrypoint: python
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[tools]
|
||||
prek = "0.4.3"
|
||||
prek = "0.3.2"
|
||||
|
||||
[settings]
|
||||
# Disable automatic trust prompts - we trust this config
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[toolchain]
|
||||
channel = "1.95.0"
|
||||
components = ["rustfmt", "clippy"]
|
||||
profile = "default"
|
||||
@@ -71,7 +71,6 @@ impl ControllerTrait for Controller {
|
||||
|
||||
let messages = vec![LLMMessage {
|
||||
author: LLMAuthor::User,
|
||||
sender_id: None,
|
||||
content: LLMMessageContent::Text("Hello!".to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
}];
|
||||
@@ -109,7 +108,6 @@ impl ControllerTrait for Controller {
|
||||
} else {
|
||||
Some(LLMMessage {
|
||||
author: LLMAuthor::Prompt,
|
||||
sender_id: None,
|
||||
content: LLMMessageContent::Text(prompt_text),
|
||||
timestamp: chrono::Utc::now(),
|
||||
})
|
||||
|
||||
@@ -28,13 +28,6 @@ pub(super) fn create_anthropic_message_request(llm_messages: Vec<LLMMessage>) ->
|
||||
},
|
||||
}]
|
||||
}
|
||||
LLMMessageContent::File(file_details) => {
|
||||
tracing::warn!(
|
||||
"The Anthropic provider's library does not support file/document content. This file message ({}) will be skipped.",
|
||||
file_details.filename(),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let message = Message { role, content };
|
||||
|
||||
@@ -84,7 +84,7 @@ impl Default for TextGenerationConfig {
|
||||
}
|
||||
|
||||
fn default_text_model_id() -> String {
|
||||
"gpt-5.4".to_owned()
|
||||
"gpt-5.2".to_owned()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
|
||||
@@ -67,7 +67,6 @@ impl ControllerTrait for Controller {
|
||||
|
||||
let messages = vec![LLMMessage {
|
||||
author: LLMAuthor::User,
|
||||
sender_id: None,
|
||||
content: LLMMessageContent::Text("Hello!".to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
}];
|
||||
@@ -105,7 +104,6 @@ impl ControllerTrait for Controller {
|
||||
} else {
|
||||
Some(LLMMessage {
|
||||
author: LLMAuthor::Prompt,
|
||||
sender_id: None,
|
||||
content: LLMMessageContent::Text(prompt_text),
|
||||
timestamp: chrono::Utc::now(),
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use async_openai::types::responses::{
|
||||
EasyInputContent, EasyInputMessage, ImageDetail, InputContent, InputFileArgs,
|
||||
InputImageContent, InputItem, InputParam, MessageType, Role,
|
||||
EasyInputContent, EasyInputMessage, ImageDetail, InputContent, InputImageContent, InputItem,
|
||||
InputParam, MessageType, Role,
|
||||
};
|
||||
|
||||
use crate::conversation::llm::{
|
||||
@@ -35,28 +35,12 @@ pub fn convert_llm_messages_to_openai_response_input(
|
||||
file_id: None,
|
||||
})])
|
||||
}
|
||||
LLMMessageContent::File(file_details) => {
|
||||
let file_data = format!(
|
||||
"data:{};base64,{}",
|
||||
file_details.mime,
|
||||
base64_encode(&file_details.data)
|
||||
);
|
||||
|
||||
let file_content = InputFileArgs::default()
|
||||
.file_data(file_data)
|
||||
.filename(file_details.filename())
|
||||
.build()
|
||||
.expect("Failed to build InputFileContent");
|
||||
|
||||
EasyInputContent::ContentList(vec![InputContent::InputFile(file_content)])
|
||||
}
|
||||
};
|
||||
|
||||
items.push(InputItem::EasyMessage(EasyInputMessage {
|
||||
r#type: MessageType::Message,
|
||||
role,
|
||||
content,
|
||||
phase: None,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,6 @@ impl ControllerTrait for Controller {
|
||||
|
||||
let messages = vec![LLMMessage {
|
||||
author: LLMAuthor::User,
|
||||
sender_id: None,
|
||||
content: LLMMessageContent::Text("Hello!".to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
}];
|
||||
@@ -102,7 +101,6 @@ impl ControllerTrait for Controller {
|
||||
} else {
|
||||
Some(LLMMessage {
|
||||
author: LLMAuthor::Prompt,
|
||||
sender_id: None,
|
||||
content: LLMMessageContent::Text(prompt_text),
|
||||
timestamp: chrono::Utc::now(),
|
||||
})
|
||||
|
||||
@@ -40,12 +40,6 @@ fn convert_llm_message_to_openai_message(llm_message: LLMMessage) -> Option<Mess
|
||||
);
|
||||
None
|
||||
}
|
||||
LLMMessageContent::File(_file_details) => {
|
||||
tracing::warn!(
|
||||
"The OpenAI-compat provider's library does not support file content. This file message will be skipped."
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ use crate::{
|
||||
entity::roomconfig::{
|
||||
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType,
|
||||
TextGenerationSenderContextMode, TextToSpeechBotMessagesFlowType,
|
||||
TextToSpeechUserMessagesFlowType,
|
||||
TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -49,9 +48,6 @@ pub enum ConfigTextGenerationSettingRelatedControllerType {
|
||||
|
||||
GetTemperatureOverride,
|
||||
SetTemperatureOverride(Option<f32>),
|
||||
|
||||
GetSenderContextMode,
|
||||
SetSenderContextMode(Option<TextGenerationSenderContextMode>),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
|
||||
@@ -163,26 +163,6 @@ fn determine_controller() {
|
||||
),
|
||||
)),
|
||||
},
|
||||
TestCase {
|
||||
name: "per-room text-generation/sender-context-mode getter",
|
||||
input: "room text-generation sender-context-mode",
|
||||
expected: super::ControllerType::Config(controller_type::ConfigControllerType::SettingsRelated(
|
||||
controller_type::SettingsStorageSource::Room,
|
||||
controller_type::ConfigSettingRelatedControllerType::TextGeneration(
|
||||
controller_type::ConfigTextGenerationSettingRelatedControllerType::GetSenderContextMode,
|
||||
),
|
||||
)),
|
||||
},
|
||||
TestCase {
|
||||
name: "global text-generation/sender-context-mode getter",
|
||||
input: "global text-generation sender-context-mode",
|
||||
expected: super::ControllerType::Config(controller_type::ConfigControllerType::SettingsRelated(
|
||||
controller_type::SettingsStorageSource::Global,
|
||||
controller_type::ConfigSettingRelatedControllerType::TextGeneration(
|
||||
controller_type::ConfigTextGenerationSettingRelatedControllerType::GetSenderContextMode,
|
||||
),
|
||||
)),
|
||||
},
|
||||
TestCase {
|
||||
name: "per-room text-to-speech/speed-override getter",
|
||||
input: "room text-to-speech speed-override",
|
||||
|
||||
@@ -3,10 +3,7 @@ mod tests;
|
||||
|
||||
use crate::{
|
||||
controller::ControllerType,
|
||||
entity::roomconfig::{
|
||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType,
|
||||
TextGenerationSenderContextMode,
|
||||
},
|
||||
entity::roomconfig::{TextGenerationAutoUsage, TextGenerationPrefixRequirementType},
|
||||
strings,
|
||||
};
|
||||
|
||||
@@ -200,43 +197,5 @@ pub(super) fn determine(
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(remaining_text) = text.strip_prefix("sender-context-mode") {
|
||||
let remaining_text = remaining_text.trim();
|
||||
|
||||
if !remaining_text.is_empty() {
|
||||
return Err(ControllerType::Error(
|
||||
strings::cfg::configuration_getter_used_with_extra_text(
|
||||
"sender-context-mode",
|
||||
remaining_text,
|
||||
)
|
||||
.to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
return Ok(ConfigTextGenerationSettingRelatedControllerType::GetSenderContextMode);
|
||||
}
|
||||
|
||||
if let Some(value_string) = text.strip_prefix("set-sender-context-mode") {
|
||||
let value_string = value_string.trim().to_owned();
|
||||
let value_choice = if value_string.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let value_choice =
|
||||
TextGenerationSenderContextMode::from_str(&value_string.to_lowercase());
|
||||
|
||||
if value_choice.is_none() {
|
||||
return Err(ControllerType::Error(
|
||||
strings::cfg::configuration_value_unrecognized(&value_string).to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
value_choice
|
||||
};
|
||||
|
||||
return Ok(
|
||||
ConfigTextGenerationSettingRelatedControllerType::SetSenderContextMode(value_choice),
|
||||
);
|
||||
}
|
||||
|
||||
Err(ControllerType::Unknown)
|
||||
}
|
||||
|
||||
@@ -90,74 +90,6 @@ fn determine_controller_context_management() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn determine_controller_sender_context() {
|
||||
use super::ConfigTextGenerationSettingRelatedControllerType;
|
||||
use super::ControllerType;
|
||||
use crate::entity::roomconfig::TextGenerationSenderContextMode;
|
||||
|
||||
struct TestCase {
|
||||
name: &'static str,
|
||||
input: &'static str,
|
||||
expected: Result<ConfigTextGenerationSettingRelatedControllerType, ControllerType>,
|
||||
}
|
||||
|
||||
let test_cases = vec![
|
||||
TestCase {
|
||||
name: "sender-context-mode getter ok",
|
||||
input: "sender-context-mode",
|
||||
expected: Ok(ConfigTextGenerationSettingRelatedControllerType::GetSenderContextMode),
|
||||
},
|
||||
TestCase {
|
||||
name: "sender-context-mode getter extra args",
|
||||
input: "sender-context-mode some values here",
|
||||
expected: Err(ControllerType::Error(
|
||||
crate::strings::cfg::configuration_getter_used_with_extra_text(
|
||||
"sender-context-mode",
|
||||
"some values here",
|
||||
),
|
||||
)),
|
||||
},
|
||||
TestCase {
|
||||
name: "sender-context-mode setter matrix_user_id",
|
||||
input: "set-sender-context-mode matrix_user_id",
|
||||
expected: Ok(
|
||||
ConfigTextGenerationSettingRelatedControllerType::SetSenderContextMode(Some(
|
||||
TextGenerationSenderContextMode::MatrixUserId,
|
||||
)),
|
||||
),
|
||||
},
|
||||
TestCase {
|
||||
name: "sender-context-mode setter uppercase",
|
||||
input: "set-sender-context-mode MATRIX_USER_ID_AND_TIMESTAMP",
|
||||
expected: Ok(
|
||||
ConfigTextGenerationSettingRelatedControllerType::SetSenderContextMode(Some(
|
||||
TextGenerationSenderContextMode::MatrixUserIdAndTimestamp,
|
||||
)),
|
||||
),
|
||||
},
|
||||
TestCase {
|
||||
name: "sender-context-mode setter invalid",
|
||||
input: "set-sender-context-mode non-Enum-Value",
|
||||
expected: Err(ControllerType::Error(
|
||||
crate::strings::cfg::configuration_value_unrecognized("non-Enum-Value"),
|
||||
)),
|
||||
},
|
||||
TestCase {
|
||||
name: "sender-context-mode unsetter",
|
||||
input: "set-sender-context-mode",
|
||||
expected: Ok(
|
||||
ConfigTextGenerationSettingRelatedControllerType::SetSenderContextMode(None),
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
for test_case in test_cases {
|
||||
let result = super::determine(test_case.input);
|
||||
assert_eq!(result, test_case.expected, "Test case: {}", test_case.name);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn determine_controller_prefix_requirement_type() {
|
||||
use super::ConfigTextGenerationSettingRelatedControllerType;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::entity::roomconfig::{
|
||||
RoomSettings, TextGenerationAutoUsage, TextGenerationPrefixRequirementType,
|
||||
TextGenerationSenderContextMode,
|
||||
};
|
||||
use crate::{Bot, entity::MessageContext};
|
||||
|
||||
@@ -152,38 +151,5 @@ pub(super) async fn dispatch(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ConfigTextGenerationSettingRelatedControllerType::GetSenderContextMode => {
|
||||
let value = &room_settings.text_generation.sender_context_mode;
|
||||
setting_get::<TextGenerationSenderContextMode>(bot, message_context, value).await
|
||||
}
|
||||
ConfigTextGenerationSettingRelatedControllerType::SetSenderContextMode(value) => {
|
||||
let value = value.to_owned();
|
||||
|
||||
let setter_callback = Box::new(move |room_settings: &mut RoomSettings| {
|
||||
room_settings.text_generation.sender_context_mode = value;
|
||||
});
|
||||
|
||||
match config_type {
|
||||
SettingsStorageSource::Room => {
|
||||
room_setting_set::<TextGenerationSenderContextMode>(
|
||||
bot,
|
||||
message_context,
|
||||
&value,
|
||||
setter_callback,
|
||||
)
|
||||
.await
|
||||
}
|
||||
SettingsStorageSource::Global => {
|
||||
global_setting_set::<TextGenerationSenderContextMode>(
|
||||
bot,
|
||||
message_context,
|
||||
&value,
|
||||
setter_callback,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ use crate::{
|
||||
roomconfig::{
|
||||
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType,
|
||||
TextGenerationSenderContextMode, TextToSpeechBotMessagesFlowType,
|
||||
TextToSpeechUserMessagesFlowType,
|
||||
TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType,
|
||||
},
|
||||
},
|
||||
strings,
|
||||
@@ -234,46 +233,6 @@ fn build_section_text_generation(command_prefix: &str, bot_username: &str) -> St
|
||||
));
|
||||
message.push_str("\n\n");
|
||||
|
||||
// Sender Context
|
||||
|
||||
message.push_str(&format!(
|
||||
"#### {}",
|
||||
strings::help::cfg::text_generation_sender_context_heading()
|
||||
));
|
||||
message.push_str("\n\n");
|
||||
message.push_str(&strings::help::cfg::text_generation_sender_context_intro());
|
||||
message.push('\n');
|
||||
message.push_str(
|
||||
&strings::help::cfg::the_following_configuration_values_are_recognized(
|
||||
TextGenerationSenderContextMode::choices(),
|
||||
),
|
||||
);
|
||||
message.push_str("\n\n");
|
||||
message.push_str(&format!(
|
||||
"- {}",
|
||||
&strings::help::cfg::current_setting_show(
|
||||
command_prefix,
|
||||
"text-generation sender-context-mode"
|
||||
)
|
||||
));
|
||||
message.push('\n');
|
||||
message.push_str(&format!(
|
||||
"- {}",
|
||||
&strings::help::cfg::current_setting_set(
|
||||
command_prefix,
|
||||
"text-generation set-sender-context-mode VALUE"
|
||||
)
|
||||
));
|
||||
message.push('\n');
|
||||
message.push_str(&format!(
|
||||
"- {}",
|
||||
&strings::help::cfg::current_setting_unset(
|
||||
command_prefix,
|
||||
"text-generation set-sender-context-mode"
|
||||
)
|
||||
));
|
||||
message.push_str("\n\n");
|
||||
|
||||
// Prompt override
|
||||
|
||||
message.push_str(&format!(
|
||||
|
||||
@@ -359,33 +359,6 @@ async fn generate_text_generation_section(
|
||||
),
|
||||
);
|
||||
|
||||
// Sender Context
|
||||
|
||||
let effective_sender_context = room_config_context.text_generation_sender_context_mode();
|
||||
let room_config_sender_context = room_config_context
|
||||
.room_config
|
||||
.settings
|
||||
.text_generation
|
||||
.sender_context_mode;
|
||||
let global_config_sender_context = room_config_context
|
||||
.global_config
|
||||
.fallback_room_settings
|
||||
.text_generation
|
||||
.sender_context_mode;
|
||||
|
||||
let sender_context_set_where = if room_config_sender_context.is_some() {
|
||||
strings::cfg::status_badge_set_in_room_config()
|
||||
} else if global_config_sender_context.is_some() {
|
||||
strings::cfg::status_badge_set_in_global_config()
|
||||
} else {
|
||||
strings::cfg::status_badge_using_hardcoded_default()
|
||||
};
|
||||
|
||||
message.push_str(&strings::cfg::status_text_generation_entry_sender_context(
|
||||
effective_sender_context,
|
||||
sender_context_set_where,
|
||||
));
|
||||
|
||||
// Prompt override
|
||||
|
||||
let text_agent_prompt = if let Some(text_generation_agent) = &text_generation_agent {
|
||||
|
||||
@@ -15,8 +15,7 @@ use crate::conversation::matrix::MatrixMessageProcessingParams;
|
||||
use crate::entity::MessagePayload;
|
||||
use crate::entity::roomconfig::{
|
||||
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
||||
TextGenerationSenderContextMode, TextToSpeechBotMessagesFlowType,
|
||||
TextToSpeechUserMessagesFlowType,
|
||||
TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType,
|
||||
};
|
||||
use crate::strings;
|
||||
use crate::utils::text_to_speech::create_transcribed_message_text;
|
||||
@@ -24,7 +23,6 @@ use crate::{
|
||||
Bot,
|
||||
conversation::{
|
||||
create_llm_conversation_for_matrix_reply_chain, create_llm_conversation_for_matrix_thread,
|
||||
llm::{Author, Conversation, MessageContent},
|
||||
matrix::create_list_of_bot_user_prefixes_to_strip,
|
||||
},
|
||||
entity::MessageContext,
|
||||
@@ -43,8 +41,6 @@ pub enum ChatCompletionControllerType {
|
||||
|
||||
Image,
|
||||
|
||||
File,
|
||||
|
||||
ThreadMention,
|
||||
ReplyMention,
|
||||
}
|
||||
@@ -423,8 +419,7 @@ async fn handle_stage_text_generation(
|
||||
| ChatCompletionControllerType::TextMention
|
||||
| ChatCompletionControllerType::TextDirect
|
||||
| ChatCompletionControllerType::Audio
|
||||
| ChatCompletionControllerType::Image
|
||||
| ChatCompletionControllerType::File => {
|
||||
| ChatCompletionControllerType::Image => {
|
||||
Some(message_context.combined_admin_and_user_regexes())
|
||||
}
|
||||
|
||||
@@ -488,13 +483,6 @@ async fn handle_stage_text_generation(
|
||||
}
|
||||
};
|
||||
|
||||
let conversation = inject_sender_context(
|
||||
conversation,
|
||||
message_context
|
||||
.room_config_context()
|
||||
.text_generation_sender_context_mode(),
|
||||
);
|
||||
|
||||
tracing::debug!(
|
||||
agent_id = agent.identifier().as_string(),
|
||||
provider = format!("{}", agent.definition().provider.clone()),
|
||||
@@ -770,238 +758,3 @@ async fn generate_and_send_tts_for_message(
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn inject_sender_context(
|
||||
conversation: Conversation,
|
||||
sender_context_mode: TextGenerationSenderContextMode,
|
||||
) -> Conversation {
|
||||
if sender_context_mode == TextGenerationSenderContextMode::Disabled {
|
||||
return conversation;
|
||||
}
|
||||
|
||||
let include_timestamp =
|
||||
sender_context_mode == TextGenerationSenderContextMode::MatrixUserIdAndTimestamp;
|
||||
|
||||
let messages = conversation
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(|mut message| {
|
||||
if message.author == Author::Prompt {
|
||||
return message;
|
||||
}
|
||||
|
||||
let Some(sender_id) = &message.sender_id else {
|
||||
return message;
|
||||
};
|
||||
|
||||
if let MessageContent::Text(ref mut text) = message.content {
|
||||
*text = if include_timestamp {
|
||||
let timestamp = message.timestamp.format("%Y-%m-%dT%H:%M:%SZ");
|
||||
format!("[sender={} sent_at={}] {}", sender_id, timestamp, text)
|
||||
} else {
|
||||
format!("[sender={}] {}", sender_id, text)
|
||||
};
|
||||
}
|
||||
|
||||
message
|
||||
})
|
||||
.collect();
|
||||
|
||||
Conversation { messages }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod sender_context_tests {
|
||||
use super::inject_sender_context;
|
||||
use crate::conversation::llm::{Author, Conversation, ImageDetails, Message, MessageContent};
|
||||
use crate::entity::roomconfig::TextGenerationSenderContextMode;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use mxlink::matrix_sdk::ruma::events::room::message::ImageMessageEventContent;
|
||||
use mxlink::matrix_sdk::ruma::{OwnedMxcUri, OwnedUserId};
|
||||
use mxlink::mime;
|
||||
|
||||
#[test]
|
||||
fn test_inject_sender_context_prefixes_text_messages() {
|
||||
let timestamp = Utc.with_ymd_and_hms(2026, 3, 23, 14, 30, 0).unwrap();
|
||||
let user_id = OwnedUserId::try_from("@alice:example.com").unwrap();
|
||||
|
||||
let conversation = Conversation {
|
||||
messages: vec![Message {
|
||||
author: Author::User,
|
||||
sender_id: Some(user_id),
|
||||
timestamp,
|
||||
content: MessageContent::Text("Hello bot".to_string()),
|
||||
}],
|
||||
};
|
||||
|
||||
let result = inject_sender_context(
|
||||
conversation,
|
||||
TextGenerationSenderContextMode::MatrixUserIdAndTimestamp,
|
||||
);
|
||||
|
||||
assert_eq!(result.messages.len(), 1);
|
||||
assert_eq!(
|
||||
result.messages[0].content,
|
||||
MessageContent::Text(
|
||||
"[sender=@alice:example.com sent_at=2026-03-23T14:30:00Z] Hello bot".to_string()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inject_sender_context_can_prefix_without_timestamp() {
|
||||
let timestamp = Utc.with_ymd_and_hms(2026, 3, 23, 14, 30, 0).unwrap();
|
||||
let user_id = OwnedUserId::try_from("@alice:example.com").unwrap();
|
||||
|
||||
let conversation = Conversation {
|
||||
messages: vec![Message {
|
||||
author: Author::User,
|
||||
sender_id: Some(user_id),
|
||||
timestamp,
|
||||
content: MessageContent::Text("Hello bot".to_string()),
|
||||
}],
|
||||
};
|
||||
|
||||
let result =
|
||||
inject_sender_context(conversation, TextGenerationSenderContextMode::MatrixUserId);
|
||||
|
||||
assert_eq!(result.messages.len(), 1);
|
||||
assert_eq!(
|
||||
result.messages[0].content,
|
||||
MessageContent::Text("[sender=@alice:example.com] Hello bot".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inject_sender_context_prefixes_assistant_messages() {
|
||||
let timestamp = Utc.with_ymd_and_hms(2026, 3, 23, 14, 30, 0).unwrap();
|
||||
let user_id = OwnedUserId::try_from("@baibot:example.com").unwrap();
|
||||
|
||||
let conversation = Conversation {
|
||||
messages: vec![Message {
|
||||
author: Author::Assistant,
|
||||
sender_id: Some(user_id),
|
||||
timestamp,
|
||||
content: MessageContent::Text("Hello human".to_string()),
|
||||
}],
|
||||
};
|
||||
|
||||
let result = inject_sender_context(
|
||||
conversation,
|
||||
TextGenerationSenderContextMode::MatrixUserIdAndTimestamp,
|
||||
);
|
||||
|
||||
assert_eq!(result.messages.len(), 1);
|
||||
assert_eq!(
|
||||
result.messages[0].content,
|
||||
MessageContent::Text(
|
||||
"[sender=@baibot:example.com sent_at=2026-03-23T14:30:00Z] Hello human".to_string()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inject_sender_context_skips_prompt_messages() {
|
||||
let timestamp = Utc.with_ymd_and_hms(2026, 3, 23, 14, 30, 0).unwrap();
|
||||
|
||||
let conversation = Conversation {
|
||||
messages: vec![Message {
|
||||
author: Author::Prompt,
|
||||
sender_id: None,
|
||||
timestamp,
|
||||
content: MessageContent::Text("You are a bot".to_string()),
|
||||
}],
|
||||
};
|
||||
|
||||
let result =
|
||||
inject_sender_context(conversation, TextGenerationSenderContextMode::MatrixUserId);
|
||||
|
||||
assert_eq!(
|
||||
result.messages[0].content,
|
||||
MessageContent::Text("You are a bot".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inject_sender_context_skips_messages_without_sender_id() {
|
||||
let timestamp = Utc.with_ymd_and_hms(2026, 3, 23, 14, 30, 0).unwrap();
|
||||
|
||||
let conversation = Conversation {
|
||||
messages: vec![Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
timestamp,
|
||||
content: MessageContent::Text("Transcribed text".to_string()),
|
||||
}],
|
||||
};
|
||||
|
||||
let result = inject_sender_context(
|
||||
conversation,
|
||||
TextGenerationSenderContextMode::MatrixUserIdAndTimestamp,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
result.messages[0].content,
|
||||
MessageContent::Text("Transcribed text".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inject_sender_context_leaves_non_text_content_unchanged() {
|
||||
let timestamp = Utc.with_ymd_and_hms(2026, 3, 23, 14, 30, 0).unwrap();
|
||||
let user_id = OwnedUserId::try_from("@alice:example.com").unwrap();
|
||||
let image_event_content = ImageMessageEventContent::plain(
|
||||
"image.png".to_string(),
|
||||
OwnedMxcUri::from("mxc://example.com/1234567890"),
|
||||
);
|
||||
|
||||
let conversation = Conversation {
|
||||
messages: vec![Message {
|
||||
author: Author::User,
|
||||
sender_id: Some(user_id),
|
||||
timestamp,
|
||||
content: MessageContent::Image(ImageDetails::new(
|
||||
image_event_content.clone(),
|
||||
mime::IMAGE_PNG,
|
||||
vec![],
|
||||
)),
|
||||
}],
|
||||
};
|
||||
|
||||
let result = inject_sender_context(
|
||||
conversation,
|
||||
TextGenerationSenderContextMode::MatrixUserIdAndTimestamp,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
result.messages[0].content,
|
||||
MessageContent::Image(ImageDetails::new(
|
||||
image_event_content,
|
||||
mime::IMAGE_PNG,
|
||||
vec![]
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inject_sender_context_none_leaves_text_unchanged() {
|
||||
let timestamp = Utc.with_ymd_and_hms(2026, 3, 23, 14, 30, 0).unwrap();
|
||||
let user_id = OwnedUserId::try_from("@alice:example.com").unwrap();
|
||||
|
||||
let conversation = Conversation {
|
||||
messages: vec![Message {
|
||||
author: Author::User,
|
||||
sender_id: Some(user_id),
|
||||
timestamp,
|
||||
content: MessageContent::Text("Hello bot".to_string()),
|
||||
}],
|
||||
};
|
||||
|
||||
let result = inject_sender_context(conversation, TextGenerationSenderContextMode::Disabled);
|
||||
|
||||
assert_eq!(
|
||||
result.messages[0].content,
|
||||
MessageContent::Text("Hello bot".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,18 +58,6 @@ pub fn determine_controller(
|
||||
)
|
||||
}
|
||||
}
|
||||
MessagePayload::File(_file_message_content) => {
|
||||
let prefix_requirement_type = message_context
|
||||
.room_config_context()
|
||||
.text_generation_prefix_requirement_type();
|
||||
|
||||
match prefix_requirement_type {
|
||||
TextGenerationPrefixRequirementType::CommandPrefix => ControllerType::Ignore,
|
||||
TextGenerationPrefixRequirementType::No => {
|
||||
ControllerType::ChatCompletion(ChatCompletionControllerType::File)
|
||||
}
|
||||
}
|
||||
}
|
||||
MessagePayload::Audio(_) => {
|
||||
ControllerType::ChatCompletion(ChatCompletionControllerType::Audio)
|
||||
}
|
||||
|
||||
@@ -64,7 +64,6 @@ mod tests {
|
||||
original_prompt: "Generate a picture of a dog",
|
||||
messages: vec![Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("Must be blue".to_owned()),
|
||||
timestamp,
|
||||
}],
|
||||
@@ -76,19 +75,16 @@ mod tests {
|
||||
messages: vec![
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("Must be blue".to_owned()),
|
||||
timestamp,
|
||||
},
|
||||
Message {
|
||||
author: Author::Assistant,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("Whatever".to_owned()),
|
||||
timestamp,
|
||||
},
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text(
|
||||
"Must be 3-legged.\nMust be flying.".to_owned(),
|
||||
),
|
||||
@@ -103,25 +99,21 @@ mod tests {
|
||||
messages: vec![
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("Must be blue".to_owned()),
|
||||
timestamp,
|
||||
},
|
||||
Message {
|
||||
author: Author::Assistant,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("Whatever".to_owned()),
|
||||
timestamp,
|
||||
},
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("Again".to_owned()),
|
||||
timestamp,
|
||||
},
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("again".to_owned()),
|
||||
timestamp,
|
||||
},
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use mxlink::matrix_sdk::ruma::OwnedUserId;
|
||||
use mxlink::matrix_sdk::ruma::events::room::message::{
|
||||
FileMessageEventContent, ImageMessageEventContent,
|
||||
};
|
||||
use mxlink::matrix_sdk::ruma::events::room::message::ImageMessageEventContent;
|
||||
use mxlink::mime::Mime;
|
||||
|
||||
use crate::agent::provider::ImageSource;
|
||||
@@ -17,7 +14,6 @@ pub enum Author {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Message {
|
||||
pub author: Author,
|
||||
pub sender_id: Option<OwnedUserId>,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub content: MessageContent,
|
||||
}
|
||||
@@ -52,35 +48,10 @@ impl From<ImageDetails> for ImageSource {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileDetails {
|
||||
pub event_content: FileMessageEventContent,
|
||||
pub mime: Mime,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl FileDetails {
|
||||
pub fn new(event_content: FileMessageEventContent, mime: Mime, data: Vec<u8>) -> Self {
|
||||
Self {
|
||||
event_content,
|
||||
mime,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filename(&self) -> String {
|
||||
self.event_content
|
||||
.filename
|
||||
.clone()
|
||||
.unwrap_or(self.event_content.body.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MessageContent {
|
||||
Text(String),
|
||||
Image(ImageDetails),
|
||||
File(FileDetails),
|
||||
}
|
||||
|
||||
impl PartialEq for MessageContent {
|
||||
@@ -91,7 +62,6 @@ impl PartialEq for MessageContent {
|
||||
// We can probably do better than this by inspecting `.event_conten1t.source`, but for now this is good enough.
|
||||
a.filename() == b.filename()
|
||||
}
|
||||
(MessageContent::File(a), MessageContent::File(b)) => a.filename() == b.filename(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -106,11 +76,6 @@ impl Conversation {
|
||||
///
|
||||
/// Certain models (like Anthropic) cannot tolerate consecutive messages by the same author,
|
||||
/// so combining them helps avoid issues.
|
||||
///
|
||||
/// When multiple text messages by the same author are merged, the resulting message keeps a
|
||||
/// `sender_id` only if all merged messages came from the same sender. Mixed-sender merges are
|
||||
/// possible for user turns in multi-user rooms, so `sender_id` is cleared in that case to
|
||||
/// avoid incorrectly attributing the whole merged turn to the first sender.
|
||||
/// See: https://github.com/etkecc/baibot/issues/13
|
||||
pub fn combine_consecutive_messages(&self) -> Conversation {
|
||||
// We'll likely get fewer messages, but let's reserve the maximum we expect.
|
||||
@@ -141,10 +106,6 @@ impl Conversation {
|
||||
text.push('\n');
|
||||
text.push_str(message_text_content);
|
||||
}
|
||||
|
||||
if last_message.sender_id != message.sender_id {
|
||||
last_message.sender_id = None;
|
||||
}
|
||||
}
|
||||
|
||||
Conversation {
|
||||
@@ -161,7 +122,7 @@ impl Conversation {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use mxlink::matrix_sdk::ruma::{OwnedMxcUri, OwnedUserId};
|
||||
use mxlink::matrix_sdk::ruma::OwnedMxcUri;
|
||||
use mxlink::mime;
|
||||
|
||||
#[test]
|
||||
@@ -184,25 +145,21 @@ mod tests {
|
||||
// User's turn
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("Hello".to_string()),
|
||||
timestamp: timestamp_1,
|
||||
},
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("How are you?".to_string()),
|
||||
timestamp: timestamp_2,
|
||||
},
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("I'm OK, btw.".to_string()),
|
||||
timestamp: timestamp_3,
|
||||
},
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Image(ImageDetails::new(
|
||||
image_event_content.clone(),
|
||||
mime::IMAGE_PNG,
|
||||
@@ -212,33 +169,28 @@ mod tests {
|
||||
},
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("Above is an image.".to_string()),
|
||||
timestamp: timestamp_4,
|
||||
},
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("Would you take a look at it?".to_string()),
|
||||
timestamp: timestamp_4,
|
||||
},
|
||||
// Assistant's turn
|
||||
Message {
|
||||
author: Author::Assistant,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("Hi there!".to_string()),
|
||||
timestamp: timestamp_2,
|
||||
},
|
||||
Message {
|
||||
author: Author::Assistant,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("I'm doing well, thank you.".to_string()),
|
||||
timestamp: timestamp_3,
|
||||
},
|
||||
// User's turn
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("That's great!".to_string()),
|
||||
timestamp: timestamp_3,
|
||||
},
|
||||
@@ -287,39 +239,4 @@ mod tests {
|
||||
);
|
||||
assert_eq!(conversation.messages[4].timestamp, timestamp_3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combine_consecutive_messages_clears_sender_id_for_mixed_sender_turns() {
|
||||
let timestamp_1 = Utc.with_ymd_and_hms(2024, 9, 20, 18, 34, 15).unwrap();
|
||||
let timestamp_2 = Utc.with_ymd_and_hms(2024, 9, 20, 18, 34, 16).unwrap();
|
||||
let sender_1 = OwnedUserId::try_from("@alice:example.com").unwrap();
|
||||
let sender_2 = OwnedUserId::try_from("@bob:example.com").unwrap();
|
||||
|
||||
let conversation = Conversation {
|
||||
messages: vec![
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: Some(sender_1),
|
||||
content: MessageContent::Text("Hello".to_string()),
|
||||
timestamp: timestamp_1,
|
||||
},
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: Some(sender_2),
|
||||
content: MessageContent::Text("Hi there".to_string()),
|
||||
timestamp: timestamp_2,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let conversation = conversation.combine_consecutive_messages();
|
||||
|
||||
assert_eq!(conversation.messages.len(), 1);
|
||||
assert_eq!(conversation.messages[0].sender_id, None);
|
||||
assert_eq!(
|
||||
conversation.messages[0].content,
|
||||
MessageContent::Text("Hello\nHi there".to_string())
|
||||
);
|
||||
assert_eq!(conversation.messages[0].timestamp, timestamp_1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ fn test_messages_by_the_bot_are_identified_correctly() {
|
||||
let llm_message = convert_matrix_message_to_llm_message(&matrix_message, &bot_user_id).unwrap();
|
||||
|
||||
assert_eq!(llm_message.author, Author::Assistant);
|
||||
assert_eq!(llm_message.sender_id, Some(bot_user_id.clone()));
|
||||
assert_eq!(
|
||||
llm_message.content,
|
||||
MessageContent::Text("Hello!".to_string())
|
||||
@@ -48,7 +47,6 @@ fn test_notice_messages_by_bot_with_speech_to_text_prefix_are_cleaned_up_and_con
|
||||
let llm_message = convert_matrix_message_to_llm_message(&matrix_message, &bot_user_id).unwrap();
|
||||
|
||||
assert_eq!(llm_message.author, Author::User);
|
||||
assert_eq!(llm_message.sender_id, None);
|
||||
assert_eq!(
|
||||
llm_message.content,
|
||||
MessageContent::Text(source_message_text.to_string())
|
||||
@@ -77,30 +75,6 @@ fn test_notice_error_messages_by_bot_are_ignored() {
|
||||
assert!(llm_message.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_messages_preserve_sender_id() {
|
||||
let bot_user_id =
|
||||
OwnedUserId::try_from("@bot:example.com").expect("Failed to parse bot user ID");
|
||||
|
||||
let user_id = OwnedUserId::try_from("@alice:example.com").expect("Failed to parse user ID");
|
||||
|
||||
let matrix_message = super::super::matrix::MatrixMessage {
|
||||
sender_id: user_id.clone(),
|
||||
content: super::super::matrix::MatrixMessageContent::Text("Hello!".to_owned()),
|
||||
mentioned_users: vec![],
|
||||
timestamp: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let llm_message = convert_matrix_message_to_llm_message(&matrix_message, &bot_user_id).unwrap();
|
||||
|
||||
assert_eq!(llm_message.author, Author::User);
|
||||
assert_eq!(llm_message.sender_id, Some(user_id));
|
||||
assert_eq!(
|
||||
llm_message.content,
|
||||
MessageContent::Text("Hello!".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_other_notice_messages_by_the_bot_are_ignored() {
|
||||
// Also see `test_notice_error_messages_by_bot_are_ignored()`.
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use tiktoken_rs::CoreBPE;
|
||||
use tiktoken_rs::bpe_for_tokenizer;
|
||||
use tiktoken_rs::get_bpe_from_tokenizer;
|
||||
use tiktoken_rs::tokenizer;
|
||||
|
||||
use super::{Author, Message, MessageContent};
|
||||
|
||||
fn get_bpe_for_model(model: &str) -> &'static CoreBPE {
|
||||
fn get_bpe_for_model(model: &str) -> CoreBPE {
|
||||
let tokenizer = tokenizer::get_tokenizer(model)
|
||||
.or_else(|| tokenizer::get_tokenizer("gpt-4"))
|
||||
.unwrap();
|
||||
|
||||
bpe_for_tokenizer(tokenizer).unwrap()
|
||||
get_bpe_from_tokenizer(tokenizer).unwrap()
|
||||
}
|
||||
|
||||
pub fn shorten_messages_list_to_context_size(
|
||||
@@ -26,7 +26,7 @@ pub fn shorten_messages_list_to_context_size(
|
||||
// We want to retain the prompt in all cases, so we always count it first.
|
||||
// We also always reserve enough tokens for the maximum response we expect.
|
||||
let mut current_context_length: u32 = if let Some(prompt_message) = prompt_message {
|
||||
calculate_token_size_for_message(bpe, model, prompt_message)
|
||||
calculate_token_size_for_message(&bpe, model, prompt_message)
|
||||
+ max_response_tokens.unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
@@ -37,7 +37,7 @@ pub fn shorten_messages_list_to_context_size(
|
||||
let mut messages_to_keep: Vec<Message> = Vec::new();
|
||||
|
||||
for message in messages {
|
||||
let tokens_for_message = calculate_token_size_for_message(bpe, model, &message);
|
||||
let tokens_for_message = calculate_token_size_for_message(&bpe, model, &message);
|
||||
|
||||
if current_context_length + tokens_for_message > max_context_tokens {
|
||||
break;
|
||||
@@ -74,7 +74,6 @@ fn calculate_token_size_for_message(bpe: &CoreBPE, model: &str, message: &Messag
|
||||
let text_length = match &message.content {
|
||||
MessageContent::Text(text) => bpe.encode_with_special_tokens(text).len() as i32,
|
||||
MessageContent::Image(..) => 0,
|
||||
MessageContent::File(..) => 0,
|
||||
};
|
||||
|
||||
(text_length + role_length + tokens_per_message + tokens_per_name) as u32
|
||||
@@ -89,12 +88,11 @@ pub mod test {
|
||||
|
||||
let message = super::Message {
|
||||
author: super::Author::User,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text("Hello there!".to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let tokens = super::calculate_token_size_for_message(bpe, model, &message);
|
||||
let tokens = super::calculate_token_size_for_message(&bpe, model, &message);
|
||||
|
||||
assert_eq!(8, tokens);
|
||||
}
|
||||
@@ -109,7 +107,6 @@ pub mod test {
|
||||
|
||||
let prompt = super::Message {
|
||||
author: super::Author::Prompt,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text("You are a bot!".to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
};
|
||||
@@ -117,14 +114,13 @@ pub mod test {
|
||||
|
||||
assert_eq!(
|
||||
prompt_length,
|
||||
super::calculate_token_size_for_message(bpe, model, &prompt)
|
||||
super::calculate_token_size_for_message(&bpe, model, &prompt)
|
||||
);
|
||||
|
||||
let mut conversation_messages = Vec::new();
|
||||
|
||||
let first = super::Message {
|
||||
author: super::Author::User,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text("Hello there!".to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
};
|
||||
@@ -132,14 +128,13 @@ pub mod test {
|
||||
|
||||
assert_eq!(
|
||||
first_length,
|
||||
super::calculate_token_size_for_message(bpe, model, &first)
|
||||
super::calculate_token_size_for_message(&bpe, model, &first)
|
||||
);
|
||||
|
||||
conversation_messages.push(first);
|
||||
|
||||
let second = super::Message {
|
||||
author: super::Author::Assistant,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text("Hello!".to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
};
|
||||
@@ -147,14 +142,13 @@ pub mod test {
|
||||
|
||||
assert_eq!(
|
||||
second_length,
|
||||
super::calculate_token_size_for_message(bpe, model, &second)
|
||||
super::calculate_token_size_for_message(&bpe, model, &second)
|
||||
);
|
||||
|
||||
conversation_messages.push(second);
|
||||
|
||||
let third = super::Message {
|
||||
author: super::Author::User,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text(
|
||||
"This is the 3rd message in this conversation. It shall be preserved.".to_owned(),
|
||||
),
|
||||
@@ -164,14 +158,13 @@ pub mod test {
|
||||
|
||||
assert_eq!(
|
||||
third_length,
|
||||
super::calculate_token_size_for_message(bpe, model, &third)
|
||||
super::calculate_token_size_for_message(&bpe, model, &third)
|
||||
);
|
||||
|
||||
conversation_messages.push(third.clone());
|
||||
|
||||
let forth = super::Message {
|
||||
author: super::Author::Assistant,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text(
|
||||
"This is yet another message that shall be preserved.".to_owned(),
|
||||
),
|
||||
@@ -181,7 +174,7 @@ pub mod test {
|
||||
|
||||
assert_eq!(
|
||||
forth_length,
|
||||
super::calculate_token_size_for_message(bpe, model, &forth)
|
||||
super::calculate_token_size_for_message(&bpe, model, &forth)
|
||||
);
|
||||
|
||||
conversation_messages.push(forth.clone());
|
||||
@@ -219,7 +212,6 @@ pub mod test {
|
||||
|
||||
let prompt = super::Message {
|
||||
author: super::Author::User,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text("あなたはボットです。".to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
};
|
||||
@@ -227,14 +219,13 @@ pub mod test {
|
||||
|
||||
assert_eq!(
|
||||
prompt_length,
|
||||
super::calculate_token_size_for_message(bpe, model, &prompt)
|
||||
super::calculate_token_size_for_message(&bpe, model, &prompt)
|
||||
);
|
||||
|
||||
let mut conversation_messages = Vec::new();
|
||||
|
||||
let first = super::Message {
|
||||
author: super::Author::User,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text("こんにちは!".to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
};
|
||||
@@ -242,14 +233,13 @@ pub mod test {
|
||||
|
||||
assert_eq!(
|
||||
first_length,
|
||||
super::calculate_token_size_for_message(bpe, model, &first)
|
||||
super::calculate_token_size_for_message(&bpe, model, &first)
|
||||
);
|
||||
|
||||
conversation_messages.push(first);
|
||||
|
||||
let second = super::Message {
|
||||
author: super::Author::Assistant,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text("こんにちは。今日は元気ですか。".to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
};
|
||||
@@ -257,14 +247,13 @@ pub mod test {
|
||||
|
||||
assert_eq!(
|
||||
second_length,
|
||||
super::calculate_token_size_for_message(bpe, model, &second)
|
||||
super::calculate_token_size_for_message(&bpe, model, &second)
|
||||
);
|
||||
|
||||
conversation_messages.push(second);
|
||||
|
||||
let third = super::Message {
|
||||
author: super::Author::User,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text(
|
||||
"これは第3のメッセージなので、保存されます。".to_string(),
|
||||
),
|
||||
@@ -274,14 +263,13 @@ pub mod test {
|
||||
|
||||
assert_eq!(
|
||||
third_length,
|
||||
super::calculate_token_size_for_message(bpe, model, &third)
|
||||
super::calculate_token_size_for_message(&bpe, model, &third)
|
||||
);
|
||||
|
||||
conversation_messages.push(third.clone());
|
||||
|
||||
let forth = super::Message {
|
||||
author: super::Author::Assistant,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text(
|
||||
"これはもう一つの保存されますメッセージです。".to_string(),
|
||||
),
|
||||
@@ -291,7 +279,7 @@ pub mod test {
|
||||
|
||||
assert_eq!(
|
||||
forth_length,
|
||||
super::calculate_token_size_for_message(bpe, model, &forth)
|
||||
super::calculate_token_size_for_message(&bpe, model, &forth)
|
||||
);
|
||||
|
||||
conversation_messages.push(forth.clone());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use mxlink::matrix_sdk::ruma::OwnedUserId;
|
||||
|
||||
use super::entity::{Author, FileDetails, ImageDetails, Message, MessageContent};
|
||||
use super::entity::{Author, ImageDetails, Message, MessageContent};
|
||||
use crate::conversation::matrix::{MatrixMessage, MatrixMessageContent};
|
||||
use crate::utils::text_to_speech as text_to_speech_utils;
|
||||
|
||||
@@ -17,17 +17,14 @@ pub fn convert_matrix_message_to_llm_message(
|
||||
|
||||
fn convert_bot_message(matrix_message: &MatrixMessage) -> Option<Message> {
|
||||
match &matrix_message.content {
|
||||
MatrixMessageContent::Text(text) => convert_bot_text_message(
|
||||
text,
|
||||
&matrix_message.timestamp,
|
||||
matrix_message.sender_id.clone(),
|
||||
),
|
||||
MatrixMessageContent::Text(text) => {
|
||||
convert_bot_text_message(text, &matrix_message.timestamp)
|
||||
}
|
||||
MatrixMessageContent::Notice(text) => {
|
||||
convert_bot_notice_message(text, &matrix_message.timestamp)
|
||||
}
|
||||
MatrixMessageContent::Image(image_content, mime_type, media_bytes) => Some(Message {
|
||||
author: Author::Assistant,
|
||||
sender_id: Some(matrix_message.sender_id.clone()),
|
||||
content: MessageContent::Image(ImageDetails::new(
|
||||
image_content.clone(),
|
||||
mime_type.clone(),
|
||||
@@ -35,27 +32,15 @@ fn convert_bot_message(matrix_message: &MatrixMessage) -> Option<Message> {
|
||||
)),
|
||||
timestamp: matrix_message.timestamp.to_owned(),
|
||||
}),
|
||||
MatrixMessageContent::File(file_content, mime_type, media_bytes) => Some(Message {
|
||||
author: Author::Assistant,
|
||||
sender_id: Some(matrix_message.sender_id.clone()),
|
||||
content: MessageContent::File(FileDetails::new(
|
||||
file_content.clone(),
|
||||
mime_type.clone(),
|
||||
media_bytes.clone(),
|
||||
)),
|
||||
timestamp: matrix_message.timestamp.to_owned(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_bot_text_message(
|
||||
text: &str,
|
||||
timestamp: &chrono::DateTime<chrono::Utc>,
|
||||
sender_id: OwnedUserId,
|
||||
) -> Option<Message> {
|
||||
Some(Message {
|
||||
author: Author::Assistant,
|
||||
sender_id: Some(sender_id),
|
||||
content: MessageContent::Text(text.to_owned()),
|
||||
timestamp: timestamp.to_owned(),
|
||||
})
|
||||
@@ -74,10 +59,8 @@ fn convert_bot_notice_message(
|
||||
|
||||
if let Some(text) = text_to_speech_utils::parse_transcribed_message_text(text) {
|
||||
// This is a transcription message. We remove the prefix and consider it as a message sent by the user.
|
||||
// sender_id is None because the original speaker is unknown.
|
||||
return Some(Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text(text.to_owned()),
|
||||
timestamp: timestamp.to_owned(),
|
||||
});
|
||||
@@ -90,19 +73,16 @@ fn convert_user_message(matrix_message: &MatrixMessage) -> Option<Message> {
|
||||
match &matrix_message.content {
|
||||
MatrixMessageContent::Text(text) => Some(Message {
|
||||
author: Author::User,
|
||||
sender_id: Some(matrix_message.sender_id.clone()),
|
||||
content: MessageContent::Text(text.clone()),
|
||||
timestamp: matrix_message.timestamp.to_owned(),
|
||||
}),
|
||||
MatrixMessageContent::Notice(text) => Some(Message {
|
||||
author: Author::User,
|
||||
sender_id: Some(matrix_message.sender_id.clone()),
|
||||
content: MessageContent::Text(text.clone()),
|
||||
timestamp: matrix_message.timestamp.to_owned(),
|
||||
}),
|
||||
MatrixMessageContent::Image(image_content, mime_type, media_bytes) => Some(Message {
|
||||
author: Author::User,
|
||||
sender_id: Some(matrix_message.sender_id.clone()),
|
||||
content: MessageContent::Image(ImageDetails::new(
|
||||
image_content.clone(),
|
||||
mime_type.clone(),
|
||||
@@ -110,15 +90,5 @@ fn convert_user_message(matrix_message: &MatrixMessage) -> Option<Message> {
|
||||
)),
|
||||
timestamp: matrix_message.timestamp.to_owned(),
|
||||
}),
|
||||
MatrixMessageContent::File(file_content, mime_type, media_bytes) => Some(Message {
|
||||
author: Author::User,
|
||||
sender_id: Some(matrix_message.sender_id.clone()),
|
||||
content: MessageContent::File(FileDetails::new(
|
||||
file_content.clone(),
|
||||
mime_type.clone(),
|
||||
media_bytes.clone(),
|
||||
)),
|
||||
timestamp: matrix_message.timestamp.to_owned(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@ use chrono::{DateTime, Utc};
|
||||
use regex::Regex;
|
||||
|
||||
use mxlink::matrix_sdk::ruma::OwnedUserId;
|
||||
use mxlink::matrix_sdk::ruma::events::room::message::{
|
||||
FileMessageEventContent, ImageMessageEventContent,
|
||||
};
|
||||
use mxlink::matrix_sdk::ruma::events::room::message::ImageMessageEventContent;
|
||||
use mxlink::mime::Mime;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -20,7 +18,6 @@ pub enum MatrixMessageContent {
|
||||
Text(String),
|
||||
Notice(String),
|
||||
Image(ImageMessageEventContent, Mime, Vec<u8>),
|
||||
File(FileMessageEventContent, Mime, Vec<u8>),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -119,7 +119,7 @@ async fn get_matrix_messages_in_reply_chain_native(
|
||||
AnySyncMessageLikeEvent::RoomMessage(room_message) => {
|
||||
if let SyncMessageLikeEvent::Original(room_message_original) = room_message {
|
||||
match room_message_original.content.relates_to {
|
||||
Some(Relation::Reply(reply)) => Some(reply.in_reply_to.event_id.clone()),
|
||||
Some(Relation::Reply { in_reply_to }) => Some(in_reply_to.event_id.clone()),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
@@ -234,7 +234,6 @@ pub async fn convert_matrix_native_event_to_matrix_message(
|
||||
MessageType::Text(text_content) => (text_content.body.clone(), false),
|
||||
MessageType::Notice(notice_content) => (notice_content.body.clone(), true),
|
||||
MessageType::Image(image_content) => (image_content.body.clone(), false),
|
||||
MessageType::File(file_content) => (file_content.body.clone(), false),
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
@@ -292,67 +291,6 @@ pub async fn convert_matrix_native_event_to_matrix_message(
|
||||
}));
|
||||
}
|
||||
|
||||
if let MessageType::File(file_content) = &room_message.msgtype {
|
||||
let media_request = mxlink::matrix_sdk::media::MediaRequestParameters {
|
||||
source: file_content.source.to_owned(),
|
||||
format: mxlink::matrix_sdk::media::MediaFormat::File,
|
||||
};
|
||||
|
||||
let file_name = file_content
|
||||
.filename
|
||||
.clone()
|
||||
.unwrap_or(file_content.body.clone());
|
||||
|
||||
let mime_type = file_content
|
||||
.info
|
||||
.as_ref()
|
||||
.and_then(|info| info.mimetype.clone())
|
||||
.and_then(|mimetype| mimetype.parse::<mxlink::mime::Mime>().ok())
|
||||
.unwrap_or_else(|| get_mime_type_from_file_name(&file_name));
|
||||
|
||||
tracing::debug!("Determined mime type {} for file {}", mime_type, file_name);
|
||||
|
||||
if mime_type == mxlink::mime::APPLICATION_OCTET_STREAM {
|
||||
tracing::debug!(
|
||||
"Skipping file {} with unsupported MIME type {}. It will be represented as a text message.",
|
||||
file_name,
|
||||
mime_type,
|
||||
);
|
||||
|
||||
return Ok(Some(MatrixMessage {
|
||||
sender_id: matrix_native_event.sender().to_owned(),
|
||||
content: MatrixMessageContent::Text(format!(
|
||||
"[A file ({}) was attached but skipped because its content type ({}) is not supported. Let the user know.]",
|
||||
file_name, mime_type,
|
||||
)),
|
||||
mentioned_users,
|
||||
timestamp,
|
||||
}));
|
||||
}
|
||||
|
||||
let span = tracing::debug_span!("get_media_content", file_name = %file_name, mime_type = %mime_type);
|
||||
|
||||
let media_bytes = matrix_link
|
||||
.client()
|
||||
.media()
|
||||
.get_media_content(&media_request, true)
|
||||
.instrument(span)
|
||||
.await?;
|
||||
|
||||
tracing::debug!(
|
||||
"Downloaded {} bytes for file {}",
|
||||
media_bytes.len(),
|
||||
file_name
|
||||
);
|
||||
|
||||
return Ok(Some(MatrixMessage {
|
||||
sender_id: matrix_native_event.sender().to_owned(),
|
||||
content: MatrixMessageContent::File(file_content.clone(), mime_type, media_bytes),
|
||||
mentioned_users,
|
||||
timestamp,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(Some(MatrixMessage {
|
||||
sender_id: matrix_native_event.sender().to_owned(),
|
||||
content: if is_notice {
|
||||
@@ -420,11 +358,11 @@ pub async fn determine_interaction_context_for_room_event(
|
||||
)
|
||||
.await
|
||||
}
|
||||
Relation::Reply(reply) => {
|
||||
Relation::Reply { in_reply_to } => {
|
||||
determine_interaction_context_for_room_event_related_to_reply(
|
||||
current_event,
|
||||
current_event_is_mentioning_bot,
|
||||
reply.in_reply_to.event_id.clone(),
|
||||
in_reply_to.event_id.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use mxlink::matrix_sdk::ruma::events::room::message::{
|
||||
AudioMessageEventContent, FileMessageEventContent, ImageMessageEventContent, MessageType,
|
||||
TextMessageEventContent,
|
||||
AudioMessageEventContent, ImageMessageEventContent, MessageType, TextMessageEventContent,
|
||||
};
|
||||
use mxlink::matrix_sdk::ruma::{OwnedEventId, OwnedUserId};
|
||||
|
||||
@@ -31,7 +30,6 @@ pub enum MessagePayload {
|
||||
Text(TextMessageEventContent),
|
||||
Audio(AudioMessageEventContent),
|
||||
Image(ImageMessageEventContent),
|
||||
File(FileMessageEventContent),
|
||||
|
||||
Reaction {
|
||||
key: String,
|
||||
@@ -59,7 +57,6 @@ impl TryInto<MessagePayload> for MessageType {
|
||||
MessagePayload::Audio(audio_content)
|
||||
}
|
||||
MessageType::Image(image_content) => MessagePayload::Image(image_content),
|
||||
MessageType::File(file_content) => MessagePayload::File(file_content),
|
||||
other => {
|
||||
return Err(format!("Unsupported message type: {:?}", other));
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@ use super::roomconfig::RoomConfig;
|
||||
|
||||
use crate::entity::roomconfig::{
|
||||
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType, TextGenerationSenderContextMode,
|
||||
TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType,
|
||||
defaults as roomconfig_defaults,
|
||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType, TextToSpeechBotMessagesFlowType,
|
||||
TextToSpeechUserMessagesFlowType, defaults as roomconfig_defaults,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -136,20 +135,6 @@ impl RoomConfigContext {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn text_generation_sender_context_mode(&self) -> TextGenerationSenderContextMode {
|
||||
self.room_config
|
||||
.settings
|
||||
.text_generation
|
||||
.sender_context_mode
|
||||
.or({
|
||||
self.global_config
|
||||
.fallback_room_settings
|
||||
.text_generation
|
||||
.sender_context_mode
|
||||
})
|
||||
.unwrap_or(roomconfig_defaults::TEXT_GENERATION_SENDER_CONTEXT_MODE)
|
||||
}
|
||||
|
||||
pub fn text_generation_prefix_requirement_type(&self) -> TextGenerationPrefixRequirementType {
|
||||
self.room_config
|
||||
.settings
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use super::{SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages};
|
||||
use super::{
|
||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType, TextGenerationSenderContextMode,
|
||||
};
|
||||
use super::{TextGenerationAutoUsage, TextGenerationPrefixRequirementType};
|
||||
use super::{TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType};
|
||||
|
||||
pub const TEXT_GENERATION_PREFIX_REQUIREMENT_TYPE: TextGenerationPrefixRequirementType =
|
||||
@@ -9,9 +7,6 @@ pub const TEXT_GENERATION_PREFIX_REQUIREMENT_TYPE: TextGenerationPrefixRequireme
|
||||
|
||||
pub const TEXT_GENERATION_AUTO_USAGE: TextGenerationAutoUsage = TextGenerationAutoUsage::Always;
|
||||
|
||||
pub const TEXT_GENERATION_SENDER_CONTEXT_MODE: TextGenerationSenderContextMode =
|
||||
TextGenerationSenderContextMode::Disabled;
|
||||
|
||||
pub const TEXT_TO_SPEECH_BOT_MESSAGES_FLOW_TYPE: TextToSpeechBotMessagesFlowType =
|
||||
TextToSpeechBotMessagesFlowType::OnDemandForVoice;
|
||||
|
||||
|
||||
@@ -16,9 +16,7 @@ pub use handler::RoomSettingsHandler;
|
||||
pub use speech_to_text::{
|
||||
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
||||
};
|
||||
pub use text_generation::{
|
||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType, TextGenerationSenderContextMode,
|
||||
};
|
||||
pub use text_generation::{TextGenerationAutoUsage, TextGenerationPrefixRequirementType};
|
||||
pub use text_to_speech::{TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
|
||||
|
||||
@@ -14,9 +14,6 @@ pub struct RoomSettingsTextGeneration {
|
||||
/// When enabled, the bot will automatically tokenize messages and try to shorten the message context intelligently.
|
||||
pub context_management_enabled: Option<bool>,
|
||||
|
||||
/// Controls how each message in the conversation context is annotated with sender metadata.
|
||||
pub sender_context_mode: Option<TextGenerationSenderContextMode>,
|
||||
|
||||
/// Allows customizing the system prompt that the agent would use
|
||||
pub prompt_override: Option<String>,
|
||||
|
||||
@@ -114,46 +111,3 @@ impl std::fmt::Display for TextGenerationAutoUsage {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub enum TextGenerationSenderContextMode {
|
||||
#[serde(rename = "disabled")]
|
||||
Disabled,
|
||||
|
||||
#[serde(rename = "matrix_user_id")]
|
||||
MatrixUserId,
|
||||
|
||||
#[serde(rename = "matrix_user_id_and_timestamp")]
|
||||
MatrixUserIdAndTimestamp,
|
||||
}
|
||||
|
||||
impl TextGenerationSenderContextMode {
|
||||
pub fn choices() -> Vec<Self> {
|
||||
vec![
|
||||
Self::Disabled,
|
||||
Self::MatrixUserId,
|
||||
Self::MatrixUserIdAndTimestamp,
|
||||
]
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"disabled" => Some(Self::Disabled),
|
||||
"matrix_user_id" => Some(Self::MatrixUserId),
|
||||
"matrix_user_id_and_timestamp" => Some(Self::MatrixUserIdAndTimestamp),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TextGenerationSenderContextMode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
TextGenerationSenderContextMode::Disabled => write!(f, "disabled"),
|
||||
TextGenerationSenderContextMode::MatrixUserId => write!(f, "matrix_user_id"),
|
||||
TextGenerationSenderContextMode::MatrixUserIdAndTimestamp => {
|
||||
write!(f, "matrix_user_id_and_timestamp")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ use mxlink::helpers::account_data_config::RoomConfigManager as AccountDataRoomCo
|
||||
pub use entity::{RoomConfig, RoomConfigCarrierContent, RoomSettings, RoomSettingsHandler};
|
||||
pub use entity::{
|
||||
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType, TextGenerationSenderContextMode,
|
||||
TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType,
|
||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType, TextToSpeechBotMessagesFlowType,
|
||||
TextToSpeechUserMessagesFlowType,
|
||||
};
|
||||
|
||||
pub type RoomConfigurationManager =
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
// rustc 1.94+ trips a query-depth overflow when computing async layouts in
|
||||
// the matrix-sdk timeline future graph. matrix-rust-sdk PR #6489 raises the
|
||||
// limit, but `recursion_limit` is per-crate and applies to the crate currently
|
||||
// being compiled — so the consumer has to repeat it.
|
||||
#![recursion_limit = "256"]
|
||||
|
||||
mod agent;
|
||||
mod bot;
|
||||
mod controller;
|
||||
|
||||
@@ -249,13 +249,6 @@ pub fn status_text_generation_entry_context_management(value: bool, set_where: &
|
||||
format!("- ♻️ Context management: `{}` ({})\n", value, set_where)
|
||||
}
|
||||
|
||||
pub fn status_text_generation_entry_sender_context(
|
||||
value: impl std::fmt::Display,
|
||||
set_where: &str,
|
||||
) -> String {
|
||||
format!("- 👤 Sender context mode: `{}` ({})\n", value, set_where)
|
||||
}
|
||||
|
||||
pub fn status_text_generation_entry_prompt(value: &str, set_where: &str) -> String {
|
||||
let value = value.trim();
|
||||
|
||||
|
||||
@@ -132,18 +132,6 @@ pub fn text_generation_context_management_intro() -> String {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn text_generation_sender_context_heading() -> &'static str {
|
||||
"👤 Sender Context Mode"
|
||||
}
|
||||
|
||||
pub fn text_generation_sender_context_intro() -> String {
|
||||
format!(
|
||||
"{}\n{}",
|
||||
"Controls whether the bot attaches sender information to conversation messages before sending them to the model.",
|
||||
"`disabled` leaves messages unchanged, `matrix_user_id` adds `[sender=@alice:example.com]`, and `matrix_user_id_and_timestamp` adds `[sender=@alice:example.com sent_at=2026-03-23T14:30:00Z]`. Enabling this sends Matrix user IDs, and optionally timestamps, to the model provider.",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn text_generation_prompt_override_heading() -> &'static str {
|
||||
"⌨️ Prompt Override"
|
||||
}
|
||||
|
||||
@@ -17,7 +17,18 @@ pub fn get_file_extension(mime_type: &mime::Mime) -> String {
|
||||
}
|
||||
|
||||
pub fn get_mime_type_from_file_name(file_name: &str) -> mime::Mime {
|
||||
mime_guess::from_path(file_name)
|
||||
.first()
|
||||
.unwrap_or(mime::APPLICATION_OCTET_STREAM)
|
||||
let extension = file_name.rsplit('.').next().unwrap_or("");
|
||||
|
||||
match extension.to_lowercase().as_str() {
|
||||
"jpg" | "jpeg" => mime::IMAGE_JPEG,
|
||||
"png" => mime::IMAGE_PNG,
|
||||
"gif" => mime::IMAGE_GIF,
|
||||
"webp" => "image/webp".parse().unwrap(),
|
||||
"svg" => mime::IMAGE_SVG,
|
||||
"tiff" | "tif" => "image/tiff".parse().unwrap(),
|
||||
"bmp" => "image/bmp".parse().unwrap(),
|
||||
"heic" | "heif" => "image/heic".parse().unwrap(),
|
||||
"avif" => "image/avif".parse().unwrap(),
|
||||
_ => mime::APPLICATION_OCTET_STREAM,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user