Compare commits

...

59 Commits

Author SHA1 Message Date
Slavi Pantaleev
c2eb7e94bc Use conventional mxlink version requirement
Replace the unconventional wildcard lower-bound expression with a standard semver lower bound for readability and tooling consistency.
2026-03-07 10:25:01 +02:00
Slavi Pantaleev
952b75318e Add auth config unit tests
Move auth_config tests into a dedicated cfg test module file to keep production config code compact while preserving behavior coverage. The tests cover password/token mode selection, missing/both auth method rejection, missing device_id, and empty-value handling.
2026-03-07 10:15:16 +02:00
Slavi Pantaleev
ce42942343 Centralize and harden user auth config handling
Move authentication-mode resolution into typed config parsing with ConfigUserAuth,
so downstream login setup consumes validated credentials instead of re-checking raw optional fields.

Enforce explicit password-vs-token selection, validate token/device/user-id requirements in one place,
and normalize empty auth env overrides to unset values for consistent behavior across YAML and environment input.
2026-03-07 10:01:47 +02:00
Slavi Pantaleev
9a226af36f Harden auth credential selection in matrix link init
Use the same non-empty access-token criterion for auth mode selection and bind the token directly from the branch condition.
Return explicit configuration errors for missing or empty `device_id`/`password` instead of panicking, so invalid auth config fails gracefully.
2026-03-07 09:42:13 +02:00
Slavi Pantaleev
0048226dc4 Update dependencies 2026-03-07 08:50:03 +02:00
Taylor Southwick
0361f9a100 use 1.13.0 2026-03-05 23:21:27 +00:00
Taylor Southwick
1d8f2b6890 Add support for access tokens using MAS 2026-03-05 18:57:00 +00:00
renovate[bot]
afc5572d6a Update docker/login-action action to v4 2026-03-04 17:13:28 +02:00
renovate[bot]
73e13dcf2f Update docker.io/ollama/ollama Docker tag to v0.17.6 2026-03-04 07:36:04 +02:00
renovate[bot]
2bebd109b1 Update forgejo.ellis.link/continuwuation/continuwuity Docker tag to v0.5.6 2026-03-04 07:35:22 +02:00
renovate[bot]
7f7c58be1f Update Rust crate tokio to 1.50.* 2026-03-03 16:27:53 +02:00
renovate[bot]
47e5a464a0 Update docker.io/ollama/ollama Docker tag to v0.17.5 2026-03-01 08:09:47 +02:00
renovate[bot]
85f751e514 Update docker.io/ollama/ollama Docker tag to v0.17.4 2026-02-27 07:08:48 +02:00
renovate[bot]
95acad3558 Update docker.io/postgres Docker tag to v18.3 2026-02-27 06:36:26 +02:00
renovate[bot]
304056c59a Update docker.io/ollama/ollama Docker tag to v0.17.2 2026-02-27 06:36:19 +02:00
renovate[bot]
bedc0335f1 Update docker.io/ollama/ollama Docker tag to v0.17.1 2026-02-26 13:33:16 +02:00
renovate[bot]
a8be8c3c1e Update ghcr.io/element-hq/element-web Docker tag to v1.12.11 2026-02-24 16:54:27 +02:00
renovate[bot]
826fa728a9 Update ghcr.io/element-hq/synapse Docker tag to v1.148.0 2026-02-24 16:53:10 +02:00
renovate[bot]
5aef8e8b2f Update Rust crate tempfile to 3.26.* 2026-02-24 08:21:45 +02:00
renovate[bot]
f70f20181e Update Rust crate chrono to v0.4.44 2026-02-24 08:16:54 +02:00
renovate[bot]
fcdd4f39ee Update docker.io/ollama/ollama Docker tag to v0.17.0 2026-02-24 08:16:31 +02:00
renovate[bot]
891adfec49 Update Rust crate anyhow to v1.0.102 2026-02-20 08:49:48 +02:00
renovate[bot]
35ab79844b Update docker.io/ollama/ollama Docker tag to v0.16.3 2026-02-20 08:49:37 +02:00
Slavi Pantaleev
bbc122fbb1 Release 1.14.3
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 06:37:11 +02:00
renovate[bot]
2413c8b88b Update actions/checkout action to v6 2026-02-18 06:31:39 +02:00
renovate[bot]
10c3c64469 Update Rust crate async-openai to 0.33.0 2026-02-18 06:25:58 +02:00
renovate[bot]
b3307b404b Update docker.io/rust Docker tag to v1.93.1 2026-02-18 06:25:48 +02:00
Slavi Pantaleev
7a0d1e830d Add Renovate configuration for automated dependency updates
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 06:06:02 +02:00
Slavi Pantaleev
b3bd241823 Release 1.14.2
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 05:53:46 +02:00
Slavi Pantaleev
de3d8b054f Update dependencies 2026-02-18 05:44:41 +02:00
Slavi Pantaleev
0a55e276a2 Update dependencies 2026-02-18 05:40:25 +02:00
Slavi Pantaleev
1f2c65d2e6 Refactor dev services to support homeserver choice (Continuwuity or Synapse)
The dev environment previously hardcoded Synapse (bundled with Postgres
and Element Web) in a monolithic etc/services/core/ directory.

With Continuwuity now available as a lighter alternative (no external DB),
this refactors the service layout so developers choose their homeserver
once and everything derives from that choice. Continuwuity is the new
default for its smaller footprint.

Key changes:
- Break etc/services/core/ into etc/services/synapse/ and
  etc/services/element-web/, each with their own compose.yml
- Add `homeserver` variable in justfile (reads var/homeserver,
  defaults to continuwuity)
- Add `homeserver-init` recipe to persist the choice
- Use placeholders (__HOMESERVER_SERVER_NAME__, __HOMESERVER_URL__,
  __HOMESERVER_CLIENT_URL__) in config templates, resolved at
  prepare time based on the chosen homeserver
- Make services-start/stop/prepare/tail-logs delegate to the chosen
  homeserver's recipes + element-web
- Make users-prepare delegate to {homeserver}-users-prepare
- Update docs/development.md for the new homeserver choice flow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 05:28:32 +02:00
Slavi Pantaleev
3b5e4745f2 Add optional Continuwuity homeserver service for development/testing
Adds Continuwuity as an alternative to Synapse for local development,
useful for testing baibot compatibility with different homeserver
implementations. Follows the same optional service pattern as localai/ollama.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 04:47:22 +02:00
Slavi Pantaleev
407bb022d9 Release 1.14.1 2026-02-10 14:42:20 +02:00
Slavi Pantaleev
faf92cac09 Switch from deprecated serde_yaml to serde_yaml_ng
serde_yaml is deprecated and unmaintained. serde_yaml_ng is the community
fork with a compatible API, so this is a straightforward rename across the
codebase.
2026-02-10 14:34:57 +02:00
Slavi Pantaleev
a82e9a1d1f Add prek pre-commit hooks via mise, fix formatting and clippy warnings
- Add mise.toml (prek 0.3.2) and .pre-commit-config.yaml with hooks for
  trailing whitespace, end-of-file, YAML check, merge conflicts, large files,
  cargo fmt, cargo clippy (-D warnings), and unit tests
- Add prek/mise recipes to justfile
- Run cargo fmt to fix formatting issues
- Fix all clippy warnings: collapse nested if statements, derive Default for Avatar
2026-02-10 14:33:19 +02:00
Slavi Pantaleev
8f87f05a08 Update dependencies to fix security vulnerabilities
- Bump mxlink (>=1.11.0 -> >=1.12.0): pulls in fixes for time and bytes CVEs
- Bump async-openai (0.32.3 -> 0.32.4)
- Bump tempfile (3.24.* -> 3.25.*)
- Run cargo update to bump transitive dependencies, notably:
  - time (0.3.46 -> 0.3.47): fix stack exhaustion DoS
2026-02-10 14:28:38 +02:00
Slavi Pantaleev
61d18b2e13 Release 1.14.0 2026-02-04 03:19:30 +02:00
Slavi Pantaleev
d831c08306 Add support for OpenAI built-in tools (web_search, code_interpreter)
Document the new tools feature in README, features.md, and providers.md.
Update the `!bai providers` command output to show vision/tools support
consistently for all providers.

Based on #62 by @yeslayla which migrated the OpenAI provider to the
Responses API.

See: https://github.com/etkecc/baibot/pull/62
2026-02-04 03:17:19 +02:00
Slavi Pantaleev
c70387b0c3 Fix sticker generation for newer GPT image models
Sticker generation was failing when using newer GPT image models
(gpt-image-1, gpt-image-1-mini, gpt-image-1.5). The issue occurred
because stickers requested 256x256 size, but these models only support
1024x1024, 1536x1024, 1024x1536, and auto.

To reproduce, send `!bai sticker Something` to an agent configured
with a GPT image model. The error was:

  invalid_request_error: Invalid value: '256x256'. Supported values
  are: '1024x1024', '1024x1536', '1536x1024', and 'auto'. (param: size)
  (code: invalid_value)

The fix replaces the hardcoded 256x256 size override with a
`smallest_size_possible` flag, letting each provider determine the
appropriate sticker size based on the model being used.

The `openai_compat` provider still defaults to requesting 256x256 in all cases
(regardless of model name).
2026-02-04 02:39:51 +02:00
Slavi Pantaleev
38516f2e17 Update services 2026-02-04 02:28:12 +02:00
Slavi Pantaleev
5481b5a763 Update dependencies 2026-02-04 01:54:53 +02:00
Slavi Pantaleev
26bc437678 Add tools config to OpenAI example in config.yml.dist 2026-02-04 01:41:27 +02:00
Layla
ec93f1ee2a Implement OpenAI's response API and add support for built-in tools (web search & code interpreter). 2026-02-04 01:41:27 +02:00
Slavi Pantaleev
b920b6e556 Release 1.13.0 2026-01-23 00:33:29 +02:00
Slavi Pantaleev
7136d34843 Update dependencies 2026-01-23 00:23:25 +02:00
Slavi Pantaleev
e0b4a40dd8 Configure switching to cheaper models (gpt-image-1-mini) for gpt-image-1 & gpt-image-1.5 2026-01-22 22:31:51 +02:00
Slavi Pantaleev
691aeeb1c7 Upgrade Rust (1.92.0 -> 1.93.0) 2026-01-22 22:23:18 +02:00
Slavi Pantaleev
257ffae9e7 Release 1.12.0 2025-12-21 23:27:08 +02:00
Slavi Pantaleev
f7bf3d7b60 Upgrade async-openai (0.32.1 -> 0.32.2)
This brings in an important bugfix related to image generation.
Ref:
- https://github.com/64bit/async-openai/issues/507
- https://github.com/64bit/async-openai/pull/508
2025-12-21 23:21:03 +02:00
Slavi Pantaleev
3a88b0d656 Make gpt-image-1.5 the default image model for OpenAI 2025-12-21 12:53:44 +02:00
Slavi Pantaleev
08c689a889 Upgrade async-openai (0.31.1 -> 0.32.1) and adapt, adding support for gpt-image-1.5 2025-12-21 12:23:25 +02:00
Slavi Pantaleev
ae8e878817 Update services 2025-12-21 11:33:08 +02:00
Slavi Pantaleev
edbd72ece6 Release 1.11.0 2025-12-15 10:03:47 +02:00
Slavi Pantaleev
22906aa2d3 Upgrade Rust (1.91.1 -> 1.92.0) 2025-12-15 09:31:34 +02:00
Slavi Pantaleev
99bde53ef6 Upgrade services 2025-12-15 09:23:59 +02:00
Slavi Pantaleev
b3fd8e548f Minor documentation updates 2025-12-15 09:22:54 +02:00
Slavi Pantaleev
062fbbb8ef Add support for custom avatars (via file path) and for not touching the already-set avatar
This is based on the work done in https://github.com/etkecc/baibot/pull/60 by https://github.com/Fmstrat (Ben Curtis),
with various changes on top to make the code more idiomatic and flexible.

This commit squashes the following patches (newest first):

- Improve handling of `user.avatar` configuration (null & empty string being the same now) and add support for a special `keep` value
- Minor import reordering
- Simplify avatar configuration (`user.avatar.source` -> `user.avatar`)
- Combine `logo_bytes` and `mime_type` determination logic and do not fall back to default avatar if reading the custom avatar file fails
- Switch from deprecated `mime_guess::guess_mime_type(avatar_path)` to `mime_guess::from_path(avatar_path).first_or_octet_stream()`
- Relax `mime_guess` constraint and order alphabetically
- Use `mime` from `mxlink`
- Add mime-type support and switch to user.avatar.source
- (Original work by Fmstrat) Add support for custom avatars

Co-authored-by: Fmstrat <nospam@nowsci.com>
2025-12-15 08:07:24 +02:00
Slavi Pantaleev
2801c78ad9 Bump default OpenAI text-generation model (gpt-5.1 -> gpt-5.2) 2025-12-12 16:05:16 +02:00
59 changed files with 1775 additions and 786 deletions

View File

@@ -16,7 +16,7 @@ jobs:
name: Unit testing and linting name: Unit testing and linting
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- name: Install SQLite3 - name: Install SQLite3
run: sudo apt-get update && sudo apt-get install -y libsqlite3-dev run: sudo apt-get update && sudo apt-get install -y libsqlite3-dev
@@ -56,9 +56,9 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Log in to the GitHub Container registry - name: Log in to the GitHub Container registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -95,7 +95,7 @@ jobs:
steps: steps:
- name: Log in to the GitHub Container registry - name: Log in to the GitHub Container registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}

36
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,36 @@
repos:
# Fast built-in hooks (Rust-native, no dependencies)
- repo: builtin
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-merge-conflict
- id: check-added-large-files
args: ['--maxkb=1024']
# Local hooks that run project-specific tools
- repo: local
hooks:
- id: cargo-fmt-check
name: Cargo Format Check
entry: cargo fmt --all -- --check
language: system
files: '\.rs$'
pass_filenames: false
- id: cargo-clippy
name: Cargo Clippy
entry: cargo clippy -- -D warnings
language: system
files: '\.rs$'
pass_filenames: false
priority: 100
- id: test-unit
name: Unit Tests
entry: just test
language: system
files: '\.rs$'
pass_filenames: false
priority: 100

View File

@@ -1,3 +1,64 @@
# (2026-02-18) Version 1.14.3
- (**Internal Improvement**) Add [Renovate](https://docs.renovatebot.com/) configuration for automated dependency updates
- (**Internal Improvement**) Dependency updates
# (2026-02-18) Version 1.14.2
- (**Internal Improvement**) Dependency updates
- (**Internal Improvement**) Reorganize the development environment to support [Continuwuity](https://continuwuity.org/) as a homeserver choice (in addition to [Synapse](https://github.com/element-hq/synapse)). Continuwuity is now the default for its lighter footprint (no external database required). See [development docs](./docs/development.md) for details.
# (2026-02-10) Version 1.14.1
- (**Security**) Dependency updates to fix security vulnerabilities ([time](https://crates.io/crates/time) stack exhaustion DoS, [bytes](https://crates.io/crates/bytes) integer overflow), via [mxlink](https://crates.io/crates/mxlink) 1.12.0
- (**Internal Improvement**) Switch from deprecated [serde_yaml](https://crates.io/crates/serde_yaml) to its maintained fork [serde_yaml_ng](https://crates.io/crates/serde_yaml_ng)
- (**Internal Improvement**) Add [prek](https://github.com/nicholasgasior/prek) pre-commit hooks via [mise](https://mise.jdx.dev/) for automated code quality checks (formatting, clippy, tests)
- (**Internal Improvement**) Fix clippy warnings and formatting issues
# (2026-02-04) Version 1.14.0
- (**Feature**) The `openai` provider now uses OpenAI's [Responses API](https://platform.openai.com/docs/api-reference/responses) (instead of the older Chat Completions API), adding support for [🛠️ built-in tools](./docs/features.md#️-built-in-tools-openai-only) (`web_search` and `code_interpreter`). These tools are **disabled by default** and can be enabled via the `text_generation.tools` configuration (see the [sample configuration](https://github.com/etkecc/baibot/blob/c70387b0c38d8d0f30bba2179a2a21a3710dbeaf/docs/sample-provider-configs/openai.yml#L12-L15)). To enable tools on an existing agent, you need to [update the agent](./docs/agents.md#updating-agents) to re-create it with the `text_generation.tools` section added and enable the tools you need. Thanks to [Layla Manley](https://github.com/yeslayla) for the contribution in [#62](https://github.com/etkecc/baibot/pull/62)!
- (**Bugfix**) Fix sticker generation for newer GPT image models (`gpt-image-1`, `gpt-image-1-mini`, `gpt-image-1.5`) which don't support the previously hardcoded `256x256` size (minimum is `1024x1024`)
- (**Internal Improvement**) Dependency updates
# (2026-01-23) Version 1.13.0
- (**Improvement**) Extend auto-switching to support cheaper models (`gpt-image-1-mini`) for `gpt-image-1` and `gpt-image-1.5` when generating stickers ([e0b4a40](https://github.com/etkecc/baibot/commit/e0b4a40))
- (**Internal Improvement**) Upgrade Rust compiler (1.92.0 -> 1.93.0) ([691aeeb](https://github.com/etkecc/baibot/commit/691aeeb))
- (**Internal Improvement**) Dependency updates
# (2025-12-21) Version 1.12.0
- (**Improvement**) Upgrade [async-openai](https://crates.io/crates/async-openai) (0.31.1 -> 0.32.2) and add support for OpenAI's `gpt-image-1.5` model ([08c689a](https://github.com/etkecc/baibot/commit/08c689a), [f7bf3d7](https://github.com/etkecc/baibot/commit/f7bf3d7))
- (**Internal Improvement**) Dependency updates
# (2025-12-15) Version 1.11.0
- (**Feature**) Add support for custom avatars via file path and for keeping the already-set avatar (for those who wish to manage it by themselves via other means). See the [sample config](./etc/app/config.yml.dist) for details. ([062fbbb](https://github.com/etkecc/baibot/commit/062fbbb8ef9ad600db483a431c5c782402191023))
- (**Internal Improvement**) Dependency updates ([99bde53](https://github.com/etkecc/baibot/commit/99bde53ef648a5a9086a96778fde4a9dbc1ede58))
- (**Internal Improvement**) Documentation updates ([b3fd8e5](https://github.com/etkecc/baibot/commit/b3fd8e548f83fe46398ced4760d7e2bb7588c24d))
- (**Internal Improvement**) Upgrade Rust compiler (1.91.1 -> 1.92.0) ([22906aa](https://github.com/etkecc/baibot/commit/22906aa2d3cae51815fad2560a545eaa69c247b6))
# (2025-12-06) Version 1.10.0 # (2025-12-06) Version 1.10.0
- (**Internal Improvement**) Dependency updates. This version is based on [mxlink](https://crates.io/crates/mxlink)@1.11.0 (which is based on the newly released [matrix-sdk](https://crates.io/crates/matrix-sdk)@[0.16.0](https://github.com/matrix-org/matrix-rust-sdk/releases/tag/matrix-sdk-0.16.0). - (**Internal Improvement**) Dependency updates. This version is based on [mxlink](https://crates.io/crates/mxlink)@1.11.0 (which is based on the newly released [matrix-sdk](https://crates.io/crates/matrix-sdk)@[0.16.0](https://github.com/matrix-org/matrix-rust-sdk/releases/tag/matrix-sdk-0.16.0).

979
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ license = "AGPL-3.0-or-later"
readme = "README.md" readme = "README.md"
keywords = ["matrix", "chat", "bot", "AI", "LLM"] keywords = ["matrix", "chat", "bot", "AI", "LLM"]
include = ["/etc/assets/baibot-torso-768.png", "/src", "/README.md", "/CHANGELOG.md", "/LICENSE"] include = ["/etc/assets/baibot-torso-768.png", "/src", "/README.md", "/CHANGELOG.md", "/LICENSE"]
version = "1.10.0" version = "1.14.3"
edition = "2024" edition = "2024"
[lib] [lib]
@@ -17,23 +17,24 @@ path = "src/lib.rs"
[dependencies] [dependencies]
anthropic = { git = "https://github.com/etkecc/anthropic-rs.git", branch = "fix-content-block-image" } anthropic = { git = "https://github.com/etkecc/anthropic-rs.git", branch = "fix-content-block-image" }
anyhow = "1.0.*" anyhow = "1.0.*"
async-openai = { version = "0.31.1", features = ["audio", "chat-completion", "image"] } async-openai = { version = "0.33.0", features = ["audio", "chat-completion", "image", "responses"] }
base64 = "0.22.*" base64 = "0.22.*"
chrono = { version = "0.4.*", default-features = false, features = ["std", "now"] } 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. # We'd rather not depend on this, but we cannot use the ruma-events EventContent macro without it.
# We add the `native-tls` feature, because of https://github.com/etkecc/rust-mxlink/issues/1 # 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"] } matrix-sdk = { version = "0.16.0", default-features = false, features = ["native-tls"] }
mime_guess = "2.0.*"
mxidwc = "1.0.*" mxidwc = "1.0.*"
mxlink = ">=1.11.0" mxlink = ">=1.13.0"
etke_openai_api_rust = "0.1.*" etke_openai_api_rust = "0.1.*"
quick_cache = "0.6.*" quick_cache = "0.6.*"
regex = "1.12.*" regex = "1.12.*"
serde = { version = "1.0.*", features = ["derive"], default-features = false } serde = { version = "1.0.*", features = ["derive"], default-features = false }
serde_json = "1.0.*" serde_json = "1.0.*"
serde_yaml = "0.9.*" serde_yaml_ng = "0.10.*"
tempfile = "3.23.*" tempfile = "3.26.*"
tiktoken-rs = { version = "0.9.*", default-features = false } tiktoken-rs = { version = "0.9.*", default-features = false }
tokio = { version = "1.48.*", features = ["rt", "rt-multi-thread", "macros"] } tokio = { version = "1.50.*", features = ["rt", "rt-multi-thread", "macros"] }
tracing = "0.1.*" tracing = "0.1.*"
tracing-subscriber = { version = "0.3.*", features = ["env-filter"] } tracing-subscriber = { version = "0.3.*", features = ["env-filter"] }
url = "2.5.*" url = "2.5.*"

View File

@@ -4,7 +4,7 @@
# # # #
####################################### #######################################
FROM docker.io/rust:1.91.1-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 RUN apt-get update && apt-get install -y build-essential pkg-config libssl-dev libsqlite3-dev

View File

@@ -4,7 +4,7 @@
# # # #
####################################### #######################################
FROM docker.io/rust:1.91.1-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 RUN apt-get update && apt-get install -y build-essential pkg-config libssl-dev libsqlite3-dev

View File

@@ -17,7 +17,7 @@ It's influenced by [chaz](https://github.com/arcuru/chaz), but does **not** use
- Supports **different use purposes** (depending on the [☁️ provider](./docs/providers.md) & model): - Supports **different use purposes** (depending on the [☁️ provider](./docs/providers.md) & model):
- [💬 text-generation](./docs/features.md#-text-generation): communicating with you via text (though certain models may "see" images as well) - [💬 text-generation](./docs/features.md#-text-generation): communicating with you via text (though certain models may "see" images as well). The [OpenAI provider](./docs/providers.md#openai) also supports [🛠️ built-in tools](./docs/features.md#️-built-in-tools-openai-only) (web search, code interpreter)
- [🦻 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#%EF%B8%8F-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): creating and editing images based on instructions - [🖌️ image-generation](./docs/features.md#image-generation): creating and editing images based on instructions

View File

@@ -43,7 +43,8 @@ Administrators cannot be changed without adjusting the bot's configuration on th
Room-local agent managers are users privileged to **create their own [agents](./agents.md)** (see `!bai agent`) in rooms. Room-local agent managers are users privileged to **create their own [agents](./agents.md)** (see `!bai agent`) in rooms.
**⚠️ WARNING**: Letting regular users create agents which contact arbitrary network services **may be a security issue**. > [!WARNING]
> Letting regular users create agents which contact arbitrary network services **may be a security issue**.
The following commands are available: The following commands are available:
- **Show** the currently allowed users: `!bai access room-local-agent-managers` - **Show** the currently allowed users: `!bai access room-local-agent-managers`

View File

@@ -12,12 +12,15 @@ This file is created from the template found in [etc/app/config.yml.dist](../../
Certain keys can be left unset, in which case [📝 hardcoded defaults](../../src/entity/cfg/defaults.rs) would be used. Certain keys can be left unset, in which case [📝 hardcoded defaults](../../src/entity/cfg/defaults.rs) would be used.
Each configuration key found in the YAML configuration can be overridden by setting an environment variable (dots should be replaced with `_`). Example: Some configuration keys found in the YAML configuration can be overridden by setting an environment variable (dots should be replaced with `_`). Example:
- to override `command_prefix`, set an environment variable `BAIBOT_COMMAND_PREFIX` - to override `command_prefix`, set an environment variable `BAIBOT_COMMAND_PREFIX`
- to override `homeserver.server_name`, set an environment variable `BAIBOT_HOMESERVER_SERVER_NAME` - to override `homeserver.server_name`, set an environment variable `BAIBOT_HOMESERVER_SERVER_NAME`
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. You can see the list of supported environment variables in the [🦀 src/entity/cfg/env.rs](../../src/entity/cfg/env.rs) file.
> [!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.
### Dynamic configuration ### Dynamic configuration

View File

@@ -18,6 +18,27 @@ For local development, we run all dependency services in [🐋 Docker](https://w
- (Optional) an API key for some Large Language Model [☁️ provider](./providers.md) (e.g. [OpenAI](./providers.md#openai)), though we recommend using [LocalAI](#localai) or [Ollama](#ollama) for local development - (Optional) an API key for some Large Language Model [☁️ provider](./providers.md) (e.g. [OpenAI](./providers.md#openai)), though we recommend using [LocalAI](#localai) or [Ollama](#ollama) for local development
### Choosing a homeserver
The development environment supports two homeserver implementations:
- **[Continuwuity](https://continuwuity.org/)** (default) — lightweight, no external database required. Good for most development needs.
- **[Synapse](https://github.com/element-hq/synapse)** — the reference implementation, bundled with Postgres. Use this if you need Synapse-specific behavior.
To choose a homeserver (optional — defaults to Continuwuity if skipped):
```sh
just homeserver-init continuwuity # or: just homeserver-init synapse
```
The choice is stored in `var/homeserver` and affects all subsequent commands.
> **Note:** If you switch homeservers after initial setup, you will need to:
> - Delete `var/app/local/` and/or `var/app/container/` (app config and data)
> - Delete `var/services/element-web/` (to regenerate its config)
> - Re-run the prepare and user registration steps
### Getting started guide ### Getting started guide
Developing [locally](#running-locally) is possible, but requires a [Rust](https://www.rust-lang.org/) toolchain. Developing [locally](#running-locally) is possible, but requires a [Rust](https://www.rust-lang.org/) toolchain.
@@ -28,11 +49,12 @@ In any case, you will need [🐋 Docker](https://www.docker.com/) as [dependency
#### Running locally #### Running locally
1. Start the core dependency services (Postgres, Synapse, Element Web): `just services-start` 1. (Optional) Choose a homeserver: `just homeserver-init continuwuity` (or `synapse`). Default is `continuwuity`.
2. (Only the first time around) Prepare initial app configuration in `var/app/local/config.yml`: `just app-local-prepare` 2. Start the homeserver and Element Web: `just services-start`
3. (Only the first time around) [Prepare your configuration file](#prepare-your-configuration-file) 3. (Only the first time around) Prepare initial app configuration in `var/app/local/config.yml`: `just app-local-prepare`
4. (Only the first time around) Prepare initial default Matrix user accounts (`admin` and `baibot`): `just users-prepare` 4. (Only the first time around) [Prepare your configuration file](#prepare-your-configuration-file)
5. (Optional) Start additional services depending on which [agent provider you've chosen](#choosing-an-agent-provider): 5. (Only the first time around) Prepare initial default Matrix user accounts (`admin` and `baibot`): `just users-prepare`
6. (Optional) Start additional services depending on which [agent provider you've chosen](#choosing-an-agent-provider):
- for [LocalAI](#localai): - for [LocalAI](#localai):
- Start services: `just localai-start` - Start services: `just localai-start`
- Wait a while for LocalAI to start up. It has a lot of models to download. Monitor progress using `just localai-tail-logs` - Wait a while for LocalAI to start up. It has a lot of models to download. Monitor progress using `just localai-tail-logs`
@@ -40,12 +62,12 @@ In any case, you will need [🐋 Docker](https://www.docker.com/) as [dependency
- for [Ollama](#ollama): - for [Ollama](#ollama):
- Start services: `just ollama-start` - Start services: `just ollama-start`
- (Only the first time around) Pull the model configured in `agents.static_definitions` in the configuration file: `just ollama-pull-model gemma2:2b` - (Only the first time around) Pull the model configured in `agents.static_definitions` in the configuration file: `just ollama-pull-model gemma2:2b`
6. Start the bot: `just run-locally` 7. Start the bot: `just run-locally`
7. Go to http://element.127.0.0.1.nip.io:42025/ and login with `admin` / `admin` 8. Go to http://element.127.0.0.1.nip.io:42025/ and login with `admin` / `admin`
8. Create a new room and invite `@baibot:synapse.127.0.0.1.nip.io` 9. Create a new room and invite `@baibot:continuwuity.127.0.0.1.nip.io` (or `@baibot:synapse.127.0.0.1.nip.io` if using Synapse)
9. When done, stop the bot (`Ctrl` + `C`) 10. When done, stop the bot (`Ctrl` + `C`)
10. Stop the core dependency services: `just services-stop` 11. Stop the services: `just services-stop`
11. (Optional) Stop additional services: 12. (Optional) Stop additional services:
- for [LocalAI](#localai): `just localai-stop` - for [LocalAI](#localai): `just localai-stop`
- for [Ollama](#ollama): `just ollama-stop` - for [Ollama](#ollama): `just ollama-stop`
@@ -54,11 +76,12 @@ In any case, you will need [🐋 Docker](https://www.docker.com/) as [dependency
You can avoid having a [Rust](https://www.rust-lang.org/) toolchain installed locally and build/run this in a container. You can avoid having a [Rust](https://www.rust-lang.org/) toolchain installed locally and build/run this in a container.
1. Start the core dependency services (Postgres, Synapse, Element Web): `just services-start` 1. (Optional) Choose a homeserver: `just homeserver-init continuwuity` (or `synapse`). Default is `continuwuity`.
2. (Only the first time around) Prepare initial app configuration in `var/app/container/config.yml`: `just app-container-prepare` 2. Start the homeserver and Element Web: `just services-start`
3. (Only the first time around) [Prepare your configuration file](#prepare-your-configuration-file) 3. (Only the first time around) Prepare initial app configuration in `var/app/container/config.yml`: `just app-container-prepare`
4. (Only the first time around) Prepare initial default Matrix user accounts (`admin` and `baibot`): `just users-prepare` 4. (Only the first time around) [Prepare your configuration file](#prepare-your-configuration-file)
5. (Optional) Start additional services depending on which [agent provider you've chosen](#choosing-an-agent-provider): 5. (Only the first time around) Prepare initial default Matrix user accounts (`admin` and `baibot`): `just users-prepare`
6. (Optional) Start additional services depending on which [agent provider you've chosen](#choosing-an-agent-provider):
- for [LocalAI](#localai): - for [LocalAI](#localai):
- Start services: `just localai-start` - Start services: `just localai-start`
- Wait a while for LocalAI to start up. It has a lot of models to download. Monitor progress using `just localai-tail-logs` - Wait a while for LocalAI to start up. It has a lot of models to download. Monitor progress using `just localai-tail-logs`
@@ -66,12 +89,12 @@ You can avoid having a [Rust](https://www.rust-lang.org/) toolchain installed lo
- for [Ollama](#ollama): - for [Ollama](#ollama):
- Start services: `just ollama-start` - Start services: `just ollama-start`
- (Only the first time around) Pull the model configured in `agents.static_definitions` in the configuration file: `just ollama-pull-model gemma2:2b` - (Only the first time around) Pull the model configured in `agents.static_definitions` in the configuration file: `just ollama-pull-model gemma2:2b`
6. Start the bot: `just run-in-container` 7. Start the bot: `just run-in-container`
7. Go to http://element.127.0.0.1.nip.io:42025/ and login with `admin` / `admin` 8. Go to http://element.127.0.0.1.nip.io:42025/ and login with `admin` / `admin`
8. Create a new room and invite `@baibot:synapse.127.0.0.1.nip.io` 9. Create a new room and invite `@baibot:continuwuity.127.0.0.1.nip.io` (or `@baibot:synapse.127.0.0.1.nip.io` if using Synapse)
9. When done, stop the bot (`Ctrl` + `C`) 10. When done, stop the bot (`Ctrl` + `C`)
10. Stop the dependency services: `just services-stop` 11. Stop the services: `just services-stop`
11. (Optional) Stop additional services: 12. (Optional) Stop additional services:
- for [LocalAI](#localai): `just localai-stop` - for [LocalAI](#localai): `just localai-stop`
- for [Ollama](#ollama): `just ollama-stop` - for [Ollama](#ollama): `just ollama-stop`

View File

@@ -40,6 +40,23 @@ You may also wish to see:
- [📖 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
#### 🛠️ Built-in Tools (OpenAI only)
The [OpenAI provider](./providers.md#openai) supports built-in tools that extend the model's capabilities:
- [🔍 Web Search](https://platform.openai.com/docs/guides/tools-web-search) (`web_search`): allows the model to search the web for up-to-date information. [🖼️ Screenshot](./screenshots/text-generation-tools-web-search.webp)
- [💻 Code Interpreter](https://platform.openai.com/docs/guides/tools-code-interpreter) (`code_interpreter`): allows the model to write and execute Python code in a sandbox
These tools are **disabled by default** and need to be explicitly enabled in the agent's `text_generation.tools` configuration. See the [OpenAI sample configuration](https://github.com/etkecc/baibot/blob/c70387b0c38d8d0f30bba2179a2a21a3710dbeaf/docs/sample-provider-configs/openai.yml#L12-L15) for reference.
To enable tools on an existing dynamically-created agent, you need to [update the agent](./agents.md#updating-agents) to re-create it with the `text_generation.tools` section added and enable the tools you need
💡 **Note**: These tools run on OpenAI's infrastructure and may incur additional costs. Web search results include citations that are incorporated into the response.
#### On-demand involvement #### On-demand involvement
In the following 2 cases, it's useful to involve the bot in conversations on-demand: In the following 2 cases, it's useful to involve the bot in conversations on-demand:

View File

@@ -23,7 +23,7 @@ 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) (no vision), [🖌️ 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) (incl. vision, incl. [🛠️ tools](./features.md#️-built-in-tools-openai-only)), [🖌️ 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.
@@ -47,7 +47,7 @@ You don't need to choose just one though. The bot supports [mixing & matching mo
- 🆔 Identifier: `anthropic` - 🆔 Identifier: `anthropic`
- 🔗 Links: [🏠 Home page](https://www.anthropic.com/), [🌐 Wiki](https://en.wikipedia.org/wiki/Anthropic), [👤 Sign up](https://console.anthropic.com/), [📋 Models list](https://docs.anthropic.com/en/docs/about-claude/models) - 🔗 Links: [🏠 Home page](https://www.anthropic.com/), [🌐 Wiki](https://en.wikipedia.org/wiki/Anthropic), [👤 Sign up](https://console.anthropic.com/), [📋 Models list](https://docs.anthropic.com/en/docs/about-claude/models)
- 🌟 Capabilities: [💬 text-generation](./features.md#-text-generation) (incl. vision) - 🌟 Capabilities: [💬 text-generation](./features.md#-text-generation) (incl. vision, no tools)
- 🗲 Quick start: - 🗲 Quick start:
- 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`
@@ -61,7 +61,7 @@ You don't need to choose just one though. The bot supports [mixing & matching mo
- 🆔 Identifier: `groq` - 🆔 Identifier: `groq`
- 🔗 Links: [🏠 Home page](https://groq.com/), [🌐 Wiki](https://en.wikipedia.org/wiki/Groq), [👤 Sign up](https://console.groq.com/login), [📋 Models list](https://console.groq.com/docs/models) - 🔗 Links: [🏠 Home page](https://groq.com/), [🌐 Wiki](https://en.wikipedia.org/wiki/Groq), [👤 Sign up](https://console.groq.com/login), [📋 Models list](https://console.groq.com/docs/models)
- 🌟 Capabilities: [💬 text-generation](./features.md#-text-generation) (no vision), [🦻 speech-to-text](./features.md#-speech-to-text) - 🌟 Capabilities: [💬 text-generation](./features.md#-text-generation) (no vision, no tools), [🦻 speech-to-text](./features.md#-speech-to-text)
- 🗲 Quick start: - 🗲 Quick start:
- 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`
@@ -75,7 +75,7 @@ You don't need to choose just one though. The bot supports [mixing & matching mo
- 🆔 Identifier: `localai` - 🆔 Identifier: `localai`
- 🔗 Links: [🏠 Home page](https://localai.io/), [📋 Models list](https://localai.io/gallery.html) - 🔗 Links: [🏠 Home page](https://localai.io/), [📋 Models list](https://localai.io/gallery.html)
- 🌟 Capabilities: [💬 text-generation](./features.md#-text-generation) (no vision), [🗣️ text-to-speech](./features.md#️-text-to-speech), [🦻 speech-to-text](./features.md#-speech-to-text) - 🌟 Capabilities: [💬 text-generation](./features.md#-text-generation) (no vision, no tools), [🗣️ text-to-speech](./features.md#️-text-to-speech), [🦻 speech-to-text](./features.md#-speech-to-text)
- 🗲 Quick start: - 🗲 Quick start:
- 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`
@@ -89,7 +89,7 @@ You don't need to choose just one though. The bot supports [mixing & matching mo
- 🆔 Identifier: `mistral` - 🆔 Identifier: `mistral`
- 🔗 Links: [🏠 Home page](https://mistral.ai/), [🌐 Wiki](https://en.wikipedia.org/wiki/Mistral_AI), [👤 Sign up](https://auth.mistral.ai/ui/registration), [📋 Models list](https://docs.mistral.ai/getting-started/models/) - 🔗 Links: [🏠 Home page](https://mistral.ai/), [🌐 Wiki](https://en.wikipedia.org/wiki/Mistral_AI), [👤 Sign up](https://auth.mistral.ai/ui/registration), [📋 Models list](https://docs.mistral.ai/getting-started/models/)
- 🌟 Capabilities: [💬 text-generation](./features.md#-text-generation) (no vision) - 🌟 Capabilities: [💬 text-generation](./features.md#-text-generation) (no vision, no tools)
- 🗲 Quick start: - 🗲 Quick start:
- 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`
@@ -103,7 +103,7 @@ You don't need to choose just one though. The bot supports [mixing & matching mo
- 🆔 Identifier: `ollama` - 🆔 Identifier: `ollama`
- 🔗 Links: [🏠 Home page](https://ollama.com/), [📋 Models list](https://ollama.com/library) - 🔗 Links: [🏠 Home page](https://ollama.com/), [📋 Models list](https://ollama.com/library)
- 🌟 Capabilities: [💬 text-generation](./features.md#-text-generation) (no vision) - 🌟 Capabilities: [💬 text-generation](./features.md#-text-generation) (no vision, no tools)
- 🗲 Quick start: - 🗲 Quick start:
- 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`
@@ -120,7 +120,7 @@ For services which are not fully compatible with the OpenAI API, consider using
- 🆔 Identifier: `openai` - 🆔 Identifier: `openai`
- 🔗 Links: [🏠 Home page](https://openai.com/), [🌐 Wiki](https://en.wikipedia.org/wiki/OpenAI), [👤 Sign up](https://platform.openai.com/signup), [📋 Models list](https://platform.openai.com/docs/models) - 🔗 Links: [🏠 Home page](https://openai.com/), [🌐 Wiki](https://en.wikipedia.org/wiki/OpenAI), [👤 Sign up](https://platform.openai.com/signup), [📋 Models list](https://platform.openai.com/docs/models)
- 🌟 Capabilities: [🖌️ image-generation](./features.md#️-image-creation), [💬 text-generation](./features.md#-text-generation) (incl. vision), [🗣️ text-to-speech](./features.md#️-text-to-speech), [🦻 speech-to-text](./features.md#-speech-to-text) - 🌟 Capabilities: [🖌️ image-generation](./features.md#️-image-creation), [💬 text-generation](./features.md#-text-generation) (incl. vision, incl. [🛠️ tools](./features.md#️-built-in-tools-openai-only)), [🗣️ text-to-speech](./features.md#️-text-to-speech), [🦻 speech-to-text](./features.md#-speech-to-text)
- 🗲 Quick start: - 🗲 Quick start:
- 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`
@@ -137,7 +137,7 @@ Some of these popular services already have **shortcut** providers (leading to t
This provider is just as featureful as the [OpenAI](#openai) provider, but is more compatible with services which do not fully adhere to the [OpenAI API spec](https://github.com/openai/openai-openapi/). This provider is just as featureful as the [OpenAI](#openai) provider, but is more compatible with services which do not fully adhere to the [OpenAI API spec](https://github.com/openai/openai-openapi/).
- 🆔 Identifier: `openai-compatible` - 🆔 Identifier: `openai-compatible`
- 🌟 Capabilities: [🖌️ image-generation](./features.md#️-image-creation), [💬 text-generation](./features.md#-text-generation) (no vision), [🗣️ text-to-speech](./features.md#️-text-to-speech), [🦻 speech-to-text](./features.md#-speech-to-text) - 🌟 Capabilities: [🖌️ image-generation](./features.md#️-image-creation), [💬 text-generation](./features.md#-text-generation) (no vision, no tools), [🗣️ text-to-speech](./features.md#️-text-to-speech), [🦻 speech-to-text](./features.md#-speech-to-text)
- 🗲 Quick start: - 🗲 Quick start:
- 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`
@@ -151,7 +151,7 @@ This provider is just as featureful as the [OpenAI](#openai) provider, but is mo
- 🆔 Identifier: `openrouter` - 🆔 Identifier: `openrouter`
- 🔗 Links: [🏠 Home page](https://openrouter.ai/), [👤 Sign up](https://openrouter.ai/), [📋 Models list](https://openrouter.ai/models) - 🔗 Links: [🏠 Home page](https://openrouter.ai/), [👤 Sign up](https://openrouter.ai/), [📋 Models list](https://openrouter.ai/models)
- 🌟 Capabilities: [💬 text-generation](./features.md#-text-generation) (no vision) - 🌟 Capabilities: [💬 text-generation](./features.md#-text-generation) (no vision, no tools)
- 🗲 Quick start: - 🗲 Quick start:
- 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`
@@ -165,7 +165,7 @@ This provider is just as featureful as the [OpenAI](#openai) provider, but is mo
- 🆔 Identifier: `together-ai` - 🆔 Identifier: `together-ai`
- 🔗 Links: [🏠 Home page](https://www.together.ai/), [👤 Sign up](https://api.together.ai/signup), [📋 Models list](https://api.together.xyz/models) - 🔗 Links: [🏠 Home page](https://www.together.ai/), [👤 Sign up](https://api.together.ai/signup), [📋 Models list](https://api.together.xyz/models)
- 🌟 Capabilities: [💬 text-generation](./features.md#-text-generation) (no vision) - 🌟 Capabilities: [💬 text-generation](./features.md#-text-generation) (no vision, no tools)
- 🗲 Quick start: - 🗲 Quick start:
- 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`

View File

@@ -1,7 +1,7 @@
base_url: https://api.openai.com/v1 base_url: https://api.openai.com/v1
api_key: YOUR_API_KEY_HERE api_key: YOUR_API_KEY_HERE
text_generation: text_generation:
model_id: gpt-5.1 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 }}." 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 temperature: 1.0
# Reasoning models need to use `max_completion_tokens` instead of `max_response_tokens`. # Reasoning models need to use `max_completion_tokens` instead of `max_response_tokens`.
@@ -9,6 +9,10 @@ text_generation:
max_response_tokens: null max_response_tokens: null
max_completion_tokens: 128000 max_completion_tokens: 128000
max_context_tokens: 400000 max_context_tokens: 400000
# Built-in tools
tools:
web_search: false
code_interpreter: false
speech_to_text: speech_to_text:
model_id: whisper-1 model_id: whisper-1
text_to_speech: text_to_speech:
@@ -17,7 +21,7 @@ text_to_speech:
speed: 1.0 speed: 1.0
response_format: opus response_format: opus
image_generation: image_generation:
model_id: gpt-image-1 model_id: gpt-image-1.5
style: null style: null
size: null size: null
quality: null quality: null

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -1,16 +1,31 @@
homeserver: homeserver:
# The canonical homeserver domain name # The canonical homeserver domain name
server_name: synapse.127.0.0.1.nip.io server_name: __HOMESERVER_SERVER_NAME__
url: http://synapse.127.0.0.1.nip.io:42020 url: __HOMESERVER_URL__
user: user:
mxid_localpart: baibot mxid_localpart: baibot
# Authentication: set EITHER password OR access_token + device_id.
#
# Password-based login (traditional homeservers):
password: baibot password: baibot
# 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
# The name the bot uses as a display name and when it refers to itself. # The name the bot uses as a display name and when it refers to itself.
# Leave empty to use the default (baibot). # Leave empty to use the default (baibot).
name: baibot name: baibot
# An optional path to an image file to be used as a custom avatar image.
# - null or empty string: use the default avatar
# - "keep": don't touch the avatar, keep whatever is already set
# - any other value: path to a custom avatar image file
avatar: null
encryption: encryption:
# An optional passphrase to use for backing up and recovering the bot's encryption keys. # An optional passphrase to use for backing up and recovering the bot's encryption keys.
# You can use any string here. # You can use any string here.
@@ -39,7 +54,7 @@ room:
access: access:
# Space-separated list of MXID patterns which specify who is an admin. # Space-separated list of MXID patterns which specify who is an admin.
admin_patterns: admin_patterns:
- "@admin:synapse.127.0.0.1.nip.io" - "@admin:__HOMESERVER_SERVER_NAME__"
persistence: persistence:
# This is unset here, because we expect the configuration to come from an environment variable (BAIBOT_PERSISTENCE_DATA_DIR_PATH). # This is unset here, because we expect the configuration to come from an environment variable (BAIBOT_PERSISTENCE_DATA_DIR_PATH).
@@ -76,7 +91,7 @@ agents:
# base_url: https://api.openai.com/v1 # base_url: https://api.openai.com/v1
# api_key: "" # api_key: ""
# text_generation: # text_generation:
# model_id: gpt-5.1 # 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 }}." # 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 # temperature: 1.0
# # Reasoning models need to use `max_completion_tokens` instead of `max_response_tokens`. # # Reasoning models need to use `max_completion_tokens` instead of `max_response_tokens`.
@@ -84,6 +99,10 @@ agents:
# max_response_tokens: null # max_response_tokens: null
# max_completion_tokens: 128000 # max_completion_tokens: 128000
# max_context_tokens: 400000 # max_context_tokens: 400000
# # Built-in tools
# tools:
# web_search: false
# code_interpreter: false
# speech_to_text: # speech_to_text:
# model_id: whisper-1 # model_id: whisper-1
# text_to_speech: # text_to_speech:
@@ -92,7 +111,7 @@ agents:
# speed: 1.0 # speed: 1.0
# response_format: opus # response_format: opus
# image_generation: # image_generation:
# model_id: gpt-image-1 # model_id: gpt-image-1.5
# style: null # style: null
# size: null # size: null
# quality: null # quality: null
@@ -147,7 +166,7 @@ initial_global_config:
# Space-separated list of MXID patterns which specify who can use the bot. # Space-separated list of MXID patterns which specify who can use the bot.
# By default, we let anyone on the homeserver use the bot. # By default, we let anyone on the homeserver use the bot.
user_patterns: user_patterns:
- "@*:synapse.127.0.0.1.nip.io" - "@*:__HOMESERVER_SERVER_NAME__"
# Controls logging. # Controls logging.
# #

View File

@@ -0,0 +1,23 @@
services:
continuwuity:
image: forgejo.ellis.link/continuwuation/continuwuity:v0.5.6
user: "${UID}:${GID}"
restart: unless-stopped
cap_drop:
- ALL
read_only: true
environment:
CONDUWUIT_CONFIG: /etc/continuwuity/continuwuity.toml
CONDUWUIT_DATABASE_PATH: /var/lib/continuwuity
ports:
- "${SERVICE_CONTINUWUITY_BIND_PORT_CLIENT_API}:6167"
volumes:
- ../../etc/services/continuwuity/config:/etc/continuwuity:ro
- ./continuwuity/data:/var/lib/continuwuity
tmpfs:
- /tmp:rw,noexec,nosuid,size=500m
networks:
default:
name: ${NETWORK_NAME}
external: true

View File

@@ -0,0 +1,19 @@
[global]
server_name = "continuwuity.127.0.0.1.nip.io"
address = "0.0.0.0"
port = 6167
database_path = "/var/lib/continuwuity"
allow_registration = true
yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true
new_user_displayname_suffix = ""
max_request_size = 20_000_000
allow_federation = false
trusted_servers = ["matrix.org"]
log = "info,state_res=warn,rocket=off,_=off,sled=off"

View File

@@ -0,0 +1,48 @@
#!/bin/sh
set -eu
if [ $# -ne 3 ]; then
echo "Usage: $0 <env-file> <username> <password>"
exit 1
fi
ENV_FILE="$1"
USERNAME="$2"
PASSWORD="$3"
SERVER="http://$(grep '^SERVICE_CONTINUWUITY_BIND_PORT_CLIENT_API=' "${ENV_FILE}" | cut -d= -f2)"
REGISTER_URL="${SERVER}/_matrix/client/v3/register"
echo "Registering user '${USERNAME}' on ${SERVER}..."
SESSION_RESPONSE=$(curl -s -X POST "${REGISTER_URL}" \
-H 'Content-Type: application/json' \
-d "{\"username\": \"${USERNAME}\", \"password\": \"${PASSWORD}\"}")
SESSION_ID=$(echo "${SESSION_RESPONSE}" | grep -o '"session":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -z "${SESSION_ID}" ]; then
echo "Error: Could not get session ID. Response: ${SESSION_RESPONSE}"
exit 1
fi
# Determine the required auth flow from the server response.
# The first user requires m.login.registration_token (bootstrap token from logs).
# Subsequent users use m.login.dummy (open registration).
if echo "${SESSION_RESPONSE}" | grep -q 'm.login.registration_token'; then
CONTAINER_ID=$(docker ps -q --filter name=baibot-continuwuity-continuwuity)
REG_TOKEN=$(docker logs "${CONTAINER_ID}" 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep 'using the registration token' | grep -oP 'registration token \K[A-Za-z0-9]+' | head -1)
AUTH_BODY="{\"type\": \"m.login.registration_token\", \"token\": \"${REG_TOKEN}\", \"session\": \"${SESSION_ID}\"}"
else
AUTH_BODY="{\"type\": \"m.login.dummy\", \"session\": \"${SESSION_ID}\"}"
fi
RESULT=$(curl -s -X POST "${REGISTER_URL}" \
-H 'Content-Type: application/json' \
-d "{\"username\": \"${USERNAME}\", \"password\": \"${PASSWORD}\", \"auth\": ${AUTH_BODY}}")
if echo "${RESULT}" | grep -q '"user_id"'; then
echo "Successfully registered user: $(echo "${RESULT}" | grep -o '"user_id":"[^"]*"' | cut -d'"' -f4)"
else
echo "Registration failed. Response: ${RESULT}"
exit 1
fi

View File

@@ -0,0 +1,21 @@
services:
element-web:
image: ghcr.io/element-hq/element-web:v1.12.11
user: "${UID}:${GID}"
restart: unless-stopped
environment:
ELEMENT_WEB_PORT: 8080
ports:
- "${SERVICE_ELEMENT_WEB_BIND_PORT_HTTP}:8080"
volumes:
- ./element-web/config.json:/app/config.json:ro
tmpfs:
- /var/cache/nginx:rw,mode=777
- /var/run:rw,mode=777
- /tmp/element-web-config:rw,mode=777
- /etc/nginx/conf.d:rw,mode=777
networks:
default:
name: ${NETWORK_NAME}
external: true

View File

@@ -1,5 +1,5 @@
{ {
"default_hs_url": "http://synapse.127.0.0.1.nip.io:42020", "default_hs_url": "__HOMESERVER_CLIENT_URL__",
"default_is_url": "https://vector.im", "default_is_url": "https://vector.im",
"integrations_ui_url": "https://scalar.vector.im/", "integrations_ui_url": "https://scalar.vector.im/",
"integrations_rest_url": "https://scalar.vector.im/api", "integrations_rest_url": "https://scalar.vector.im/api",

View File

@@ -3,6 +3,8 @@ SERVICE_SYNAPSE_BIND_PORT_FEDERATION_API=127.0.0.1:42028
SERVICE_ELEMENT_WEB_BIND_PORT_HTTP=127.0.0.1:42025 SERVICE_ELEMENT_WEB_BIND_PORT_HTTP=127.0.0.1:42025
SERVICE_CONTINUWUITY_BIND_PORT_CLIENT_API=127.0.0.1:42030
SERVICE_OLLAMA_BIND_PORT_HTTP=127.0.0.1:42026 SERVICE_OLLAMA_BIND_PORT_HTTP=127.0.0.1:42026
# See https://localai.io/basics/container/#all-in-one-images for the list of available images # See https://localai.io/basics/container/#all-in-one-images for the list of available images

View File

@@ -1,6 +1,6 @@
services: services:
ollama: ollama:
image: docker.io/ollama/ollama:0.13.0 image: docker.io/ollama/ollama:0.17.6
restart: unless-stopped restart: unless-stopped
ports: ports:
- "${SERVICE_OLLAMA_BIND_PORT_HTTP}:11434" - "${SERVICE_OLLAMA_BIND_PORT_HTTP}:11434"

View File

@@ -1,6 +1,6 @@
services: services:
postgres: postgres:
image: docker.io/postgres:18.1-alpine image: docker.io/postgres:18.3-alpine
user: ${UID}:${GID} user: ${UID}:${GID}
restart: unless-stopped restart: unless-stopped
environment: environment:
@@ -14,7 +14,7 @@ services:
- /etc/passwd:/etc/passwd:ro - /etc/passwd:/etc/passwd:ro
synapse: synapse:
image: ghcr.io/element-hq/synapse:v1.143.0 image: ghcr.io/element-hq/synapse:v1.148.0
user: "${UID}:${GID}" user: "${UID}:${GID}"
restart: unless-stopped restart: unless-stopped
entrypoint: python entrypoint: python
@@ -23,25 +23,9 @@ services:
- "${SERVICE_SYNAPSE_BIND_PORT_CLIENT_API}:8008" - "${SERVICE_SYNAPSE_BIND_PORT_CLIENT_API}:8008"
- "${SERVICE_SYNAPSE_BIND_PORT_FEDERATION_API}:8008" - "${SERVICE_SYNAPSE_BIND_PORT_FEDERATION_API}:8008"
volumes: volumes:
- ../../etc/services/core/synapse/config:/config:ro - ../../etc/services/synapse/config:/config:ro
- ./synapse/media-store:/media-store - ./synapse/media-store:/media-store
element-web:
image: ghcr.io/element-hq/element-web:v1.12.4
user: "${UID}:${GID}"
restart: unless-stopped
environment:
ELEMENT_WEB_PORT: 8080
ports:
- "${SERVICE_ELEMENT_WEB_BIND_PORT_HTTP}:8080"
volumes:
- ../../etc/services/core/element-web/config.json:/app/config.json:ro
tmpfs:
- /var/cache/nginx:rw,mode=777
- /var/run:rw,mode=777
- /tmp/element-web-config:rw,mode=777
- /etc/nginx/conf.d:rw,mode=777
networks: networks:
default: default:
name: ${NETWORK_NAME} name: ${NETWORK_NAME}

214
justfile
View File

@@ -2,10 +2,33 @@ project_name := "baibot"
container_image_name := "localhost/baibot" container_image_name := "localhost/baibot"
project_container_network := "baibot" project_container_network := "baibot"
admin_username := "admin"
admin_password := "admin"
bot_username := "baibot"
bot_password := "baibot"
homeserver := `cat var/homeserver 2>/dev/null || echo continuwuity`
mise_data_dir := env("MISE_DATA_DIR", justfile_directory() / "var/mise")
mise_trusted_config_paths := justfile_directory() / "mise.toml"
# Show help by default # Show help by default
default: default:
@just --list --justfile {{ justfile() }} @just --list --justfile {{ justfile() }}
# Selects which homeserver implementation to use (continuwuity or synapse)
homeserver-init value:
#!/bin/sh
mkdir -p {{ justfile_directory() }}/var
echo {{ value }} > {{ justfile_directory() }}/var/homeserver
echo ""
echo "⚠️ If you had already prepared your app configuration (var/app/local/config.yml or var/app/container/config.yml),"
echo " you will need to update it manually or delete it and re-run the prepare step."
echo " You should also delete var/app/local/data and/or var/app/container/data,"
echo " as old application state is not compatible across homeserver implementations."
echo ""
echo "⚠️ If Element Web was already prepared, delete var/services/element-web/ to regenerate its config."
# Builds and runs a development binary # Builds and runs a development binary
run-locally *extra_args: app-local-prepare run-locally *extra_args: app-local-prepare
RUST_BACKTRACE=1 \ RUST_BACKTRACE=1 \
@@ -65,9 +88,13 @@ docker-compose services_type *extra_args:
-p {{ project_name }}-{{ services_type }} \ -p {{ project_name }}-{{ services_type }} \
{{ extra_args }} {{ extra_args }}
# Runs a docker-compose command against the core services # Runs a docker-compose command against the synapse services
docker-compose-core *extra_args: docker-compose-synapse *extra_args:
just docker-compose core {{ extra_args }} just docker-compose synapse {{ extra_args }}
# Runs a docker-compose command against the element-web services
docker-compose-element-web *extra_args:
just docker-compose element-web {{ extra_args }}
# Runs a docker-compose command against the localai services # Runs a docker-compose command against the localai services
docker-compose-localai *extra_args: docker-compose-localai *extra_args:
@@ -77,17 +104,52 @@ docker-compose-localai *extra_args:
docker-compose-ollama *extra_args: docker-compose-ollama *extra_args:
just docker-compose ollama {{ extra_args }} just docker-compose ollama {{ extra_args }}
# Runs all core dependency components (in the background) # Runs a docker-compose command against the continuwuity services
services-start: services-prepare (docker-compose-core "up" "-d") docker-compose-continuwuity *extra_args:
just docker-compose continuwuity {{ extra_args }}
# Stops all core dependency components # Runs the homeserver and Element Web (in the background)
services-stop: (docker-compose-core "down") services-start: services-prepare
just -f {{ justfile_directory() }}/justfile {{ homeserver }}-start
just -f {{ justfile_directory() }}/justfile element-web-start
# Tails the logs for all running core services # Stops Element Web and the homeserver
services-tail-logs: (docker-compose-core "logs" "-f") services-stop:
just -f {{ justfile_directory() }}/justfile element-web-stop
just -f {{ justfile_directory() }}/justfile {{ homeserver }}-stop
# Prepares the core services for running # Tails the logs for the homeserver and Element Web
services-prepare: _prepare-var-services-env _prepare-var-services-postgres _prepare-var-services-synapse _prepare-container-network services-tail-logs:
just -f {{ justfile_directory() }}/justfile {{ homeserver }}-tail-logs
# Prepares the homeserver and Element Web for running
services-prepare:
just -f {{ justfile_directory() }}/justfile {{ homeserver }}-prepare
just -f {{ justfile_directory() }}/justfile element-web-prepare
# Runs Synapse (in the background)
synapse-start: synapse-prepare (docker-compose-synapse "up" "-d")
# Stops Synapse
synapse-stop: (docker-compose-synapse "down")
# Tails the logs for Synapse
synapse-tail-logs: (docker-compose-synapse "logs" "-f")
# Prepares Synapse for running
synapse-prepare: _prepare-var-services-env _prepare-var-services-postgres _prepare-var-services-synapse _prepare-container-network
# Runs Element Web (in the background)
element-web-start: element-web-prepare (docker-compose-element-web "up" "-d")
# Stops Element Web
element-web-stop: (docker-compose-element-web "down")
# Tails the logs for Element Web
element-web-tail-logs: (docker-compose-element-web "logs" "-f")
# Prepares Element Web for running
element-web-prepare: _prepare-var-services-env _prepare-var-services-element-web _prepare-container-network
# Runs LocalAI (in the background) # Runs LocalAI (in the background)
localai-start: localai-prepare (docker-compose-localai "up" "-d") localai-start: localai-prepare (docker-compose-localai "up" "-d")
@@ -113,6 +175,27 @@ ollama-tail-logs: (docker-compose-ollama "logs" "-f")
# Prepares Ollama for running # Prepares Ollama for running
ollama-prepare: _prepare-var-services-env _prepare-var-services-ollama _prepare-container-network ollama-prepare: _prepare-var-services-env _prepare-var-services-ollama _prepare-container-network
# Runs Continuwuity (in the background)
continuwuity-start: continuwuity-prepare (docker-compose-continuwuity "up" "-d")
# Stops Continuwuity
continuwuity-stop: (docker-compose-continuwuity "down")
# Tails the logs for Continuwuity
continuwuity-tail-logs: (docker-compose-continuwuity "logs" "-f")
# Prepares Continuwuity for running
continuwuity-prepare: _prepare-var-services-env _prepare-var-services-continuwuity _prepare-container-network
# Registers a user on Continuwuity via the Matrix Client-Server API
continuwuity-register-user username password:
{{ justfile_directory() }}/etc/services/continuwuity/register-user.sh {{ justfile_directory() }}/var/services/env {{ username }} {{ password }}
# Prepares the Continuwuity user accounts
continuwuity-users-prepare: continuwuity-prepare
just -f {{ justfile_directory() }}/justfile continuwuity-register-user "{{ admin_username }}" "{{ admin_password }}"
just -f {{ justfile_directory() }}/justfile continuwuity-register-user "{{ bot_username }}" "{{ bot_password }}"
# Pulls an Ollama model # Pulls an Ollama model
ollama-pull-model model_id: ollama-pull-model model_id:
just -f {{ justfile_directory() }}/justfile docker-compose-ollama \ just -f {{ justfile_directory() }}/justfile docker-compose-ollama \
@@ -126,16 +209,20 @@ app-local-prepare: _prepare-var-app-local-config_yml _prepare-var-app-local-data
app-container-prepare: _prepare-var-app-container-config_yml _prepare-var-app-container-data app-container-prepare: _prepare-var-app-container-config_yml _prepare-var-app-container-data
# Prepares the user accounts # Prepares the user accounts
users-prepare: services-prepare users-prepare:
just -f {{ justfile_directory() }}/justfile synapse-register-admin-user "admin" "admin" just -f {{ justfile_directory() }}/justfile {{ homeserver }}-users-prepare
just -f {{ justfile_directory() }}/justfile synapse-register-regular-user "baibot" "baibot"
# Prepares the Synapse user accounts
synapse-users-prepare: synapse-prepare
just -f {{ justfile_directory() }}/justfile synapse-register-admin-user "{{ admin_username }}" "{{ admin_password }}"
just -f {{ justfile_directory() }}/justfile synapse-register-regular-user "{{ bot_username }}" "{{ bot_password }}"
# Starts a Postgres CLI (psql) # Starts a Postgres CLI (psql)
postgres-cli: services-prepare (docker-compose-core "exec" "postgres" "/bin/sh" "-c" "'PGUSER=synapse PGPASSWORD=synapse-password PGDATABASE=homeserver psql -h postgres'") postgres-cli: synapse-prepare (docker-compose-synapse "exec" "postgres" "/bin/sh" "-c" "'PGUSER=synapse PGPASSWORD=synapse-password PGDATABASE=homeserver psql -h postgres'")
# Creates an administrator user # Creates an administrator user on Synapse
synapse-register-admin-user username password: services-prepare synapse-register-admin-user username password: synapse-prepare
just -f {{ justfile_directory() }}/justfile docker-compose-core \ just -f {{ justfile_directory() }}/justfile docker-compose-synapse \
exec synapse \ exec synapse \
register_new_matrix_user \ register_new_matrix_user \
--admin \ --admin \
@@ -144,9 +231,9 @@ synapse-register-admin-user username password: services-prepare
-c /config/homeserver.yaml \ -c /config/homeserver.yaml \
http://localhost:8008 http://localhost:8008
# Create a regular user # Creates a regular user on Synapse
synapse-register-regular-user username password: services-prepare synapse-register-regular-user username password: synapse-prepare
just -f {{ justfile_directory() }}/justfile docker-compose-core \ just -f {{ justfile_directory() }}/justfile docker-compose-synapse \
exec synapse \ exec synapse \
register_new_matrix_user \ register_new_matrix_user \
--no-admin \ --no-admin \
@@ -159,6 +246,44 @@ synapse-register-regular-user username password: services-prepare
clippy *extra_args: clippy *extra_args:
cargo clippy {{ extra_args }} cargo clippy {{ extra_args }}
# Checks that the code compiles without building
check:
cargo check
# Invokes mise with the project-local data directory
mise *args: _ensure_mise_data_directory
#!/bin/sh
export MISE_DATA_DIR="{{ mise_data_dir }}"
export MISE_TRUSTED_CONFIG_PATHS="{{ mise_trusted_config_paths }}"
mise {{ args }}
# Runs prek (pre-commit hooks manager) with the given arguments
prek *args: _ensure_mise_tools_installed
@just --justfile {{ justfile() }} mise exec -- prek {{ args }}
# Runs pre-commit hooks on staged files
prek-run-on-staged *args: _ensure_mise_tools_installed
@just --justfile {{ justfile() }} mise exec -- prek run {{ args }}
# Runs pre-commit hooks on all files
prek-run-on-all *args: _ensure_mise_tools_installed
@just --justfile {{ justfile() }} mise exec -- prek run --all-files {{ args }}
# Installs the git pre-commit hook (runs prek automatically before each commit)
prek-install-git-pre-commit-hook: _ensure_mise_tools_installed
@just --justfile {{ justfile() }} mise exec -- prek install
# Internal - ensures var/mise directory exists
_ensure_mise_data_directory:
#!/bin/sh
if [ ! -d "{{ mise_data_dir }}" ]; then
mkdir -p "{{ mise_data_dir }}"
fi
# Internal - ensures mise tools are installed
_ensure_mise_tools_installed: _ensure_mise_data_directory
@just --justfile {{ justfile() }} mise install --quiet
_prepare-var-services-env: _prepare-var-services-env:
#!/bin/sh #!/bin/sh
cd {{ justfile_directory() }}; cd {{ justfile_directory() }};
@@ -188,6 +313,22 @@ _prepare-var-services-synapse:
mkdir -p var/services/synapse/media-store mkdir -p var/services/synapse/media-store
fi fi
_prepare-var-services-element-web:
#!/bin/sh
cd {{ justfile_directory() }};
if [ ! -f var/services/element-web/config.json ]; then
mkdir -p var/services/element-web
cp {{ justfile_directory() }}/etc/services/element-web/config.json.dist var/services/element-web/config.json
homeserver="{{ homeserver }}"
if [ "$homeserver" = "continuwuity" ]; then
sed --in-place 's|__HOMESERVER_CLIENT_URL__|http://continuwuity.127.0.0.1.nip.io:42030|g' var/services/element-web/config.json
elif [ "$homeserver" = "synapse" ]; then
sed --in-place 's|__HOMESERVER_CLIENT_URL__|http://synapse.127.0.0.1.nip.io:42020|g' var/services/element-web/config.json
fi
fi
_prepare-var-services-ollama: _prepare-var-services-ollama:
#!/bin/sh #!/bin/sh
cd {{ justfile_directory() }}; cd {{ justfile_directory() }};
@@ -196,6 +337,14 @@ _prepare-var-services-ollama:
mkdir -p var/services/ollama mkdir -p var/services/ollama
fi fi
_prepare-var-services-continuwuity:
#!/bin/sh
cd {{ justfile_directory() }};
if [ ! -f var/services/continuwuity ]; then
mkdir -p var/services/continuwuity/data
fi
_prepare-var-services-localai: _prepare-var-services-localai:
#!/bin/sh #!/bin/sh
cd {{ justfile_directory() }}; cd {{ justfile_directory() }};
@@ -219,6 +368,15 @@ _prepare-var-app-local-config_yml:
if [ ! -f var/app/local/config.yml ]; then if [ ! -f var/app/local/config.yml ]; then
mkdir -p var/app/local mkdir -p var/app/local
cp {{ justfile_directory() }}/etc/app/config.yml.dist var/app/local/config.yml cp {{ justfile_directory() }}/etc/app/config.yml.dist var/app/local/config.yml
homeserver="{{ homeserver }}"
if [ "$homeserver" = "continuwuity" ]; then
sed --in-place 's/__HOMESERVER_SERVER_NAME__/continuwuity.127.0.0.1.nip.io/g' var/app/local/config.yml
sed --in-place 's|__HOMESERVER_URL__|http://continuwuity.127.0.0.1.nip.io:42030|g' var/app/local/config.yml
elif [ "$homeserver" = "synapse" ]; then
sed --in-place 's/__HOMESERVER_SERVER_NAME__/synapse.127.0.0.1.nip.io/g' var/app/local/config.yml
sed --in-place 's|__HOMESERVER_URL__|http://synapse.127.0.0.1.nip.io:42020|g' var/app/local/config.yml
fi
fi fi
_prepare-var-app-local-data: _prepare-var-app-local-data:
@@ -236,7 +394,18 @@ _prepare-var-app-container-config_yml:
if [ ! -f var/app/container/config.yml ]; then if [ ! -f var/app/container/config.yml ]; then
mkdir -p var/app/container mkdir -p var/app/container
cp {{ justfile_directory() }}/etc/app/config.yml.dist var/app/container/config.yml cp {{ justfile_directory() }}/etc/app/config.yml.dist var/app/container/config.yml
sed --in-place 's/synapse.127.0.0.1.nip.io:42020/synapse:8008/g' var/app/container/config.yml
homeserver="{{ homeserver }}"
if [ "$homeserver" = "continuwuity" ]; then
sed --in-place 's/__HOMESERVER_SERVER_NAME__/continuwuity.127.0.0.1.nip.io/g' var/app/container/config.yml
sed --in-place 's|__HOMESERVER_URL__|http://continuwuity.127.0.0.1.nip.io:42030|g' var/app/container/config.yml
sed --in-place 's/continuwuity.127.0.0.1.nip.io:42030/continuwuity:6167/g' var/app/container/config.yml
elif [ "$homeserver" = "synapse" ]; then
sed --in-place 's/__HOMESERVER_SERVER_NAME__/synapse.127.0.0.1.nip.io/g' var/app/container/config.yml
sed --in-place 's|__HOMESERVER_URL__|http://synapse.127.0.0.1.nip.io:42020|g' var/app/container/config.yml
sed --in-place 's/synapse.127.0.0.1.nip.io:42020/synapse:8008/g' var/app/container/config.yml
fi
sed --in-place 's/127.0.0.1:42026/ollama:11434/g' var/app/container/config.yml sed --in-place 's/127.0.0.1:42026/ollama:11434/g' var/app/container/config.yml
sed --in-place 's/127.0.0.1:42027/localai:8080/g' var/app/container/config.yml sed --in-place 's/127.0.0.1:42027/localai:8080/g' var/app/container/config.yml
fi fi
@@ -248,4 +417,3 @@ _prepare-var-app-container-data:
if [ ! -f var/app/container/data ]; then if [ ! -f var/app/container/data ]; then
mkdir -p var/app/container/data mkdir -p var/app/container/data
fi fi

6
mise.toml Normal file
View File

@@ -0,0 +1,6 @@
[tools]
prek = "0.3.2"
[settings]
# Disable automatic trust prompts - we trust this config
yes = true

9
renovate.json Normal file
View File

@@ -0,0 +1,9 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
],
"labels": [
"dependencies"
]
}

View File

@@ -33,11 +33,11 @@ pub struct AgentDefinition {
)] )]
pub provider: AgentProvider, pub provider: AgentProvider,
pub config: serde_yaml::Value, pub config: serde_yaml_ng::Value,
} }
impl AgentDefinition { impl AgentDefinition {
pub fn new(id: String, provider: AgentProvider, config: serde_yaml::Value) -> Self { pub fn new(id: String, provider: AgentProvider, config: serde_yaml_ng::Value) -> Self {
Self { Self {
id, id,
provider, provider,

View File

@@ -15,7 +15,7 @@ pub enum Error {
// Contains the error from the constructor function // Contains the error from the constructor function
ConstructionFailed(anyhow::Error), ConstructionFailed(anyhow::Error),
// Contains the error from the YAML deserialization function // Contains the error from the YAML deserialization function
Yaml(serde_yaml::Error), Yaml(serde_yaml_ng::Error),
} }
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
@@ -69,7 +69,7 @@ pub(super) fn create(
pub fn create_from_provider_and_yaml_value_config( pub fn create_from_provider_and_yaml_value_config(
provider: &AgentProvider, provider: &AgentProvider,
identifier: &PublicIdentifier, identifier: &PublicIdentifier,
config: serde_yaml::Value, config: serde_yaml_ng::Value,
) -> Result<AgentInstance> { ) -> Result<AgentInstance> {
let definition = AgentDefinition::new(identifier.prefixless(), provider.to_owned(), config); let definition = AgentDefinition::new(identifier.prefixless(), provider.to_owned(), config);
@@ -79,7 +79,7 @@ pub fn create_from_provider_and_yaml_value_config(
fn create_controller_from_provider_and_json_value_config( fn create_controller_from_provider_and_json_value_config(
agent_id: &str, agent_id: &str,
provider: &AgentProvider, provider: &AgentProvider,
config: serde_yaml::Value, config: serde_yaml_ng::Value,
) -> Result<ControllerType> { ) -> Result<ControllerType> {
match provider { match provider {
AgentProvider::Anthropic => { AgentProvider::Anthropic => {
@@ -112,43 +112,43 @@ fn create_controller_from_provider_and_json_value_config(
} }
} }
pub fn default_config_for_provider(provider: &AgentProvider) -> serde_yaml::Value { pub fn default_config_for_provider(provider: &AgentProvider) -> serde_yaml_ng::Value {
match provider { match provider {
AgentProvider::Anthropic => { AgentProvider::Anthropic => {
let config = super::provider::anthropic::default_config(); let config = super::provider::anthropic::default_config();
serde_yaml::to_value(config).expect("Failed to serialize config") serde_yaml_ng::to_value(config).expect("Failed to serialize config")
} }
AgentProvider::Groq => { AgentProvider::Groq => {
let config = super::provider::groq::default_config(); let config = super::provider::groq::default_config();
serde_yaml::to_value(config).expect("Failed to serialize config") serde_yaml_ng::to_value(config).expect("Failed to serialize config")
} }
AgentProvider::LocalAI => { AgentProvider::LocalAI => {
let config = super::provider::localai::default_config(); let config = super::provider::localai::default_config();
serde_yaml::to_value(config).expect("Failed to serialize config") serde_yaml_ng::to_value(config).expect("Failed to serialize config")
} }
AgentProvider::Mistral => { AgentProvider::Mistral => {
let config = super::provider::mistral::default_config(); let config = super::provider::mistral::default_config();
serde_yaml::to_value(config).expect("Failed to serialize config") serde_yaml_ng::to_value(config).expect("Failed to serialize config")
} }
AgentProvider::Ollama => { AgentProvider::Ollama => {
let config = super::provider::ollama::default_config(); let config = super::provider::ollama::default_config();
serde_yaml::to_value(config).expect("Failed to serialize config") serde_yaml_ng::to_value(config).expect("Failed to serialize config")
} }
AgentProvider::OpenAI => { AgentProvider::OpenAI => {
let config = super::provider::openai::default_config(); let config = super::provider::openai::default_config();
serde_yaml::to_value(config).expect("Failed to serialize config") serde_yaml_ng::to_value(config).expect("Failed to serialize config")
} }
AgentProvider::OpenAICompat => { AgentProvider::OpenAICompat => {
let config = super::provider::openai_compat::default_config(); let config = super::provider::openai_compat::default_config();
serde_yaml::to_value(config).expect("Failed to serialize config") serde_yaml_ng::to_value(config).expect("Failed to serialize config")
} }
AgentProvider::OpenRouter => { AgentProvider::OpenRouter => {
let config = super::provider::openrouter::default_config(); let config = super::provider::openrouter::default_config();
serde_yaml::to_value(config).expect("Failed to serialize config") serde_yaml_ng::to_value(config).expect("Failed to serialize config")
} }
AgentProvider::TogetherAI => { AgentProvider::TogetherAI => {
let config = super::provider::togetherai::default_config(); let config = super::provider::togetherai::default_config();
serde_yaml::to_value(config).expect("Failed to serialize config") serde_yaml_ng::to_value(config).expect("Failed to serialize config")
} }
} }
} }

View File

@@ -146,10 +146,10 @@ impl ControllerTrait for Controller {
.temperature_override .temperature_override
.unwrap_or(text_generation_config.temperature); .unwrap_or(text_generation_config.temperature);
if let Some(prompt_message) = prompt_message { if let Some(prompt_message) = prompt_message
if let LLMMessageContent::Text(text) = &prompt_message.content { && let LLMMessageContent::Text(text) = &prompt_message.content
request.system = text.clone(); {
} request.system = text.clone();
} }
request.model = text_generation_config.model_id.clone(); request.model = text_generation_config.model_id.clone();

View File

@@ -12,12 +12,12 @@ use super::controller::ControllerType;
pub fn create_controller_from_yaml_value_config( pub fn create_controller_from_yaml_value_config(
agent_id: &str, agent_id: &str,
config: serde_yaml::Value, config: serde_yaml_ng::Value,
) -> AgentInstantiationResult<ControllerType> { ) -> AgentInstantiationResult<ControllerType> {
let config = match &config { let config = match &config {
serde_yaml::Value::Mapping(_) => { serde_yaml_ng::Value::Mapping(_) => {
let config: Config = let config: Config =
serde_yaml::from_value(config).map_err(AgentInstantiationError::Yaml)?; serde_yaml_ng::from_value(config).map_err(AgentInstantiationError::Yaml)?;
config config
.validate() .validate()

View File

@@ -69,6 +69,7 @@ impl AgentProvider {
models_list_url: Some("https://docs.anthropic.com/en/docs/about-claude/models"), models_list_url: Some("https://docs.anthropic.com/en/docs/about-claude/models"),
supported_purposes: vec![AgentPurpose::TextGeneration], supported_purposes: vec![AgentPurpose::TextGeneration],
text_generation_supports_vision: true, text_generation_supports_vision: true,
text_generation_supports_tools: false,
}, },
Self::Groq => AgentProviderInfo { Self::Groq => AgentProviderInfo {
id: Self::Groq.to_static_str(), id: Self::Groq.to_static_str(),
@@ -80,11 +81,12 @@ impl AgentProvider {
models_list_url: Some("https://console.groq.com/docs/models"), models_list_url: Some("https://console.groq.com/docs/models"),
supported_purposes: vec![AgentPurpose::TextGeneration, AgentPurpose::SpeechToText], supported_purposes: vec![AgentPurpose::TextGeneration, AgentPurpose::SpeechToText],
text_generation_supports_vision: false, text_generation_supports_vision: false,
text_generation_supports_tools: false,
}, },
Self::LocalAI => AgentProviderInfo { Self::LocalAI => AgentProviderInfo {
id: Self::LocalAI.to_static_str(), id: Self::LocalAI.to_static_str(),
name: "LocalAI", name: "LocalAI",
description: "LocalAI is the free, Open Source OpenAI alternative. LocalAI act as a drop-in replacement REST API that’s compatible with OpenAI API specifications for local inferencing. It allows you to run LLMs, generate images, audio (and not only) locally or on-prem with consumer grade hardware, supporting multiple model families and architectures.", description: "LocalAI is the free, Open Source OpenAI alternative. LocalAI act as a drop-in replacement REST API that's compatible with OpenAI API specifications for local inferencing. It allows you to run LLMs, generate images, audio (and not only) locally or on-prem with consumer grade hardware, supporting multiple model families and architectures.",
homepage_url: Some("https://localai.io/"), homepage_url: Some("https://localai.io/"),
wiki_url: None, wiki_url: None,
sign_up_url: None, sign_up_url: None,
@@ -95,6 +97,7 @@ impl AgentProvider {
AgentPurpose::SpeechToText, AgentPurpose::SpeechToText,
], ],
text_generation_supports_vision: false, text_generation_supports_vision: false,
text_generation_supports_tools: false,
}, },
Self::Mistral => AgentProviderInfo { Self::Mistral => AgentProviderInfo {
id: Self::Mistral.to_static_str(), id: Self::Mistral.to_static_str(),
@@ -106,6 +109,7 @@ impl AgentProvider {
models_list_url: Some("https://docs.mistral.ai/getting-started/models/"), models_list_url: Some("https://docs.mistral.ai/getting-started/models/"),
supported_purposes: vec![AgentPurpose::TextGeneration], supported_purposes: vec![AgentPurpose::TextGeneration],
text_generation_supports_vision: false, text_generation_supports_vision: false,
text_generation_supports_tools: false,
}, },
Self::Ollama => AgentProviderInfo { Self::Ollama => AgentProviderInfo {
id: Self::Ollama.to_static_str(), id: Self::Ollama.to_static_str(),
@@ -117,6 +121,7 @@ impl AgentProvider {
models_list_url: Some("https://ollama.com/library"), models_list_url: Some("https://ollama.com/library"),
supported_purposes: vec![AgentPurpose::TextGeneration], supported_purposes: vec![AgentPurpose::TextGeneration],
text_generation_supports_vision: false, text_generation_supports_vision: false,
text_generation_supports_tools: false,
}, },
Self::OpenAI => AgentProviderInfo { Self::OpenAI => AgentProviderInfo {
id: Self::OpenAI.to_static_str(), id: Self::OpenAI.to_static_str(),
@@ -133,6 +138,7 @@ impl AgentProvider {
AgentPurpose::SpeechToText, AgentPurpose::SpeechToText,
], ],
text_generation_supports_vision: true, text_generation_supports_vision: true,
text_generation_supports_tools: true,
}, },
Self::OpenAICompat => AgentProviderInfo { Self::OpenAICompat => AgentProviderInfo {
id: Self::OpenAICompat.to_static_str(), id: Self::OpenAICompat.to_static_str(),
@@ -149,6 +155,7 @@ impl AgentProvider {
AgentPurpose::SpeechToText, AgentPurpose::SpeechToText,
], ],
text_generation_supports_vision: false, text_generation_supports_vision: false,
text_generation_supports_tools: false,
}, },
Self::OpenRouter => AgentProviderInfo { Self::OpenRouter => AgentProviderInfo {
id: Self::OpenRouter.to_static_str(), id: Self::OpenRouter.to_static_str(),
@@ -160,6 +167,7 @@ impl AgentProvider {
models_list_url: Some("https://openrouter.ai/models"), models_list_url: Some("https://openrouter.ai/models"),
supported_purposes: vec![AgentPurpose::TextGeneration], supported_purposes: vec![AgentPurpose::TextGeneration],
text_generation_supports_vision: false, text_generation_supports_vision: false,
text_generation_supports_tools: false,
}, },
Self::TogetherAI => AgentProviderInfo { Self::TogetherAI => AgentProviderInfo {
id: Self::TogetherAI.to_static_str(), id: Self::TogetherAI.to_static_str(),
@@ -171,6 +179,7 @@ impl AgentProvider {
models_list_url: Some("https://api.together.xyz/models"), models_list_url: Some("https://api.together.xyz/models"),
supported_purposes: vec![AgentPurpose::TextGeneration], supported_purposes: vec![AgentPurpose::TextGeneration],
text_generation_supports_vision: false, text_generation_supports_vision: false,
text_generation_supports_tools: false,
}, },
} }
} }
@@ -192,4 +201,5 @@ pub struct AgentProviderInfo {
pub models_list_url: Option<&'static str>, pub models_list_url: Option<&'static str>,
pub supported_purposes: Vec<AgentPurpose>, pub supported_purposes: Vec<AgentPurpose>,
pub text_generation_supports_vision: bool, pub text_generation_supports_vision: bool,
pub text_generation_supports_tools: bool,
} }

View File

@@ -2,7 +2,7 @@ use mxlink::mime;
#[derive(Default)] #[derive(Default)]
pub struct ImageGenerationParams { pub struct ImageGenerationParams {
pub size_override: Option<String>, pub smallest_size_possible: bool,
pub cheaper_model_switching_allowed: bool, pub cheaper_model_switching_allowed: bool,
@@ -10,8 +10,8 @@ pub struct ImageGenerationParams {
} }
impl ImageGenerationParams { impl ImageGenerationParams {
pub fn with_size_override(mut self, value: Option<String>) -> Self { pub fn with_smallest_size_possible(mut self, value: bool) -> Self {
self.size_override = value; self.smallest_size_possible = value;
self self
} }

View File

@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::OPENAI_IMAGE_MODEL_GPT_IMAGE_1; use super::OPENAI_IMAGE_MODEL_GPT_IMAGE_1_DOT_5;
use crate::agent::{default_prompt, provider::ConfigTrait}; use crate::agent::{default_prompt, provider::ConfigTrait};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -64,6 +64,9 @@ pub struct TextGenerationConfig {
#[serde(default)] #[serde(default)]
pub max_context_tokens: u32, pub max_context_tokens: u32,
#[serde(default)]
pub tools: ToolsConfig,
} }
impl Default for TextGenerationConfig { impl Default for TextGenerationConfig {
@@ -75,12 +78,22 @@ impl Default for TextGenerationConfig {
max_response_tokens: None, max_response_tokens: None,
max_completion_tokens: Some(128_000), max_completion_tokens: Some(128_000),
max_context_tokens: 400_000, max_context_tokens: 400_000,
tools: ToolsConfig::default(),
} }
} }
} }
fn default_text_model_id() -> String { fn default_text_model_id() -> String {
"gpt-5.1".to_owned() "gpt-5.2".to_owned()
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ToolsConfig {
#[serde(default)]
pub web_search: bool,
#[serde(default)]
pub code_interpreter: bool,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -162,7 +175,7 @@ pub struct ImageGenerationConfig {
impl Default for ImageGenerationConfig { impl Default for ImageGenerationConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
model_id: OPENAI_IMAGE_MODEL_GPT_IMAGE_1.to_owned(), model_id: OPENAI_IMAGE_MODEL_GPT_IMAGE_1_DOT_5.to_owned(),
style: default_image_style(), style: default_image_style(),
size: default_image_size(), size: default_image_size(),
quality: default_image_quality(), quality: default_image_quality(),
@@ -178,8 +191,11 @@ impl ImageGenerationConfig {
"dall-e-2" => Ok(async_openai::types::images::ImageModel::DallE2), "dall-e-2" => Ok(async_openai::types::images::ImageModel::DallE2),
"dall-e-3" => Ok(async_openai::types::images::ImageModel::DallE3), "dall-e-3" => Ok(async_openai::types::images::ImageModel::DallE3),
"gpt-image-1" => Ok(async_openai::types::images::ImageModel::GptImage1), "gpt-image-1" => Ok(async_openai::types::images::ImageModel::GptImage1),
"gpt-image-1.5" => Ok(async_openai::types::images::ImageModel::GptImage1dot5),
"gpt-image-1-mini" => Ok(async_openai::types::images::ImageModel::GptImage1Mini), "gpt-image-1-mini" => Ok(async_openai::types::images::ImageModel::GptImage1Mini),
other => Ok(async_openai::types::images::ImageModel::Other(other.to_owned())), other => Ok(async_openai::types::images::ImageModel::Other(
other.to_owned(),
)),
} }
} }
} }

View File

@@ -5,10 +5,13 @@ use async_openai::{
config::OpenAIConfig, config::OpenAIConfig,
types::{ types::{
audio::{AudioInput, CreateSpeechRequestArgs, CreateTranscriptionRequestArgs}, audio::{AudioInput, CreateSpeechRequestArgs, CreateTranscriptionRequestArgs},
chat::{ChatCompletionRequestMessage, CreateChatCompletionRequestArgs},
images::{ images::{
CreateImageEditRequestArgs, CreateImageRequestArgs, CreateImageEditRequestArgs, CreateImageRequestArgs, Image, ImageInput, ImageModel,
Image, ImageInput, ImageModel, ImageResponseFormat, ImageResponseFormat,
},
responses::{
CodeInterpreterContainerAuto, CodeInterpreterTool, CodeInterpreterToolContainer,
CreateResponseArgs, OutputItem, OutputMessageContent, Tool, WebSearchTool,
}, },
}, },
}; };
@@ -28,12 +31,9 @@ use crate::{
use crate::{ use crate::{
agent::{ agent::{
AgentPurpose, AgentPurpose,
provider::{ provider::entity::{
entity::{ ImageEditResult, ImageGenerationResult, ImageSource, PingResult, TextToSpeechParams,
ImageEditResult, ImageGenerationResult, ImageSource, PingResult, TextToSpeechResult,
TextToSpeechParams, TextToSpeechResult,
},
openai::utils::convert_string_to_enum,
}, },
}, },
strings, strings,
@@ -129,28 +129,45 @@ impl ControllerTrait for Controller {
conversation_messages.insert(0, prompt_message); conversation_messages.insert(0, prompt_message);
} }
let openai_conversation_messages: Vec<ChatCompletionRequestMessage> = let input =
super::utils::convert_llm_messages_to_openai_messages(conversation_messages); super::utils::convert_llm_messages_to_openai_response_input(conversation_messages);
let messages_count = openai_conversation_messages.len(); let messages_count = match &input {
async_openai::types::responses::InputParam::Items(items) => items.len(),
_ => 1,
};
let temperature = params let temperature = params
.temperature_override .temperature_override
.unwrap_or(text_generation_config.temperature); .unwrap_or(text_generation_config.temperature);
let mut request_builder = CreateChatCompletionRequestArgs::default(); let mut request_builder = CreateResponseArgs::default();
request_builder request_builder
.model(&text_generation_config.model_id) .model(&text_generation_config.model_id)
.temperature(temperature) .temperature(temperature)
.messages(openai_conversation_messages); .input(input);
if let Some(max_response_tokens) = text_generation_config.max_response_tokens { let mut tools = Vec::new();
request_builder.max_tokens(max_response_tokens); if text_generation_config.tools.web_search {
tools.push(Tool::WebSearch(WebSearchTool::default()));
}
if text_generation_config.tools.code_interpreter {
tools.push(Tool::CodeInterpreter(CodeInterpreterTool {
container: CodeInterpreterToolContainer::Auto(
CodeInterpreterContainerAuto::default(),
),
}));
} }
if let Some(max_completion_tokens) = text_generation_config.max_completion_tokens { if !tools.is_empty() {
request_builder.max_completion_tokens(max_completion_tokens); request_builder.tools(tools);
}
if let Some(max_response_tokens) = text_generation_config.max_response_tokens {
request_builder.max_output_tokens(max_response_tokens);
} else if let Some(max_completion_tokens) = text_generation_config.max_completion_tokens {
request_builder.max_output_tokens(max_completion_tokens);
} }
let request = request_builder.build()?; let request = request_builder.build()?;
@@ -160,33 +177,28 @@ impl ControllerTrait for Controller {
model = format!("{:?}", request.model), model = format!("{:?}", request.model),
?messages_count, ?messages_count,
request = request_as_json, request = request_as_json,
"Sending OpenAI chat completion API request" "Sending OpenAI response API request"
); );
} }
let response = self.client.chat().create(request).await?; let response = self.client.responses().create(request).await?;
tracing::trace!( tracing::trace!(?response, "Got response from the OpenAI response API");
?response,
"Got response from the OpenAI chat completion API"
);
// We only request 1 result, so there should only be 1 choice. for item in response.output {
if let Some(choice) = response.choices.into_iter().next() { if let OutputItem::Message(message) = item {
match choice.message.content { for content in message.content {
Some(text) => { if let OutputMessageContent::OutputText(text_content) = content {
return Ok(TextGenerationResult { text }); return Ok(TextGenerationResult {
} text: text_content.text,
None => { });
return Err(anyhow::anyhow!( }
"No content was found in the response choice from the OpenAI chat completion API"
));
} }
} }
} }
Err(anyhow::anyhow!( Err(anyhow::anyhow!(
"No response messages choices were returned from the OpenAI chat completion API" "No response messages choices were returned from the OpenAI response API"
)) ))
} }
@@ -254,10 +266,10 @@ impl ControllerTrait for Controller {
match original_model { match original_model {
ImageModel::DallE2 => ImageModel::DallE2, ImageModel::DallE2 => ImageModel::DallE2,
ImageModel::DallE3 => ImageModel::DallE2, ImageModel::DallE3 => ImageModel::DallE2,
ImageModel::Other(_) => { ImageModel::GptImage1 => ImageModel::GptImage1Mini,
ImageModel::DallE2 ImageModel::GptImage1dot5 => ImageModel::GptImage1Mini,
} ImageModel::GptImage1Mini => ImageModel::GptImage1Mini,
_ => original_model.clone(), ImageModel::Other(_) => ImageModel::DallE2,
} }
} else { } else {
original_model original_model
@@ -293,10 +305,11 @@ impl ControllerTrait for Controller {
image_generation_config.quality.clone() image_generation_config.quality.clone()
}; };
let size = params let size = if params.smallest_size_possible {
.size_override Some(get_sticker_size(&model))
.map(|s| convert_string_to_enum::<async_openai::types::images::ImageSize>(&s).unwrap()) } else {
.or(image_generation_config.size); image_generation_config.size
};
let response_format = match model.clone() { let response_format = match model.clone() {
ImageModel::DallE2 => Some(ImageResponseFormat::B64Json), ImageModel::DallE2 => Some(ImageResponseFormat::B64Json),
@@ -305,6 +318,7 @@ impl ControllerTrait for Controller {
// In fact, specifying the response format results in an error. // In fact, specifying the response format results in an error.
ImageModel::GptImage1 => None, ImageModel::GptImage1 => None,
ImageModel::GptImage1Mini => None, ImageModel::GptImage1Mini => None,
ImageModel::GptImage1dot5 => None,
ImageModel::Other(_) => Some(ImageResponseFormat::B64Json), ImageModel::Other(_) => Some(ImageResponseFormat::B64Json),
}; };
@@ -390,9 +404,15 @@ impl ControllerTrait for Controller {
} }
let dalle2_size = match image_generation_config.size { let dalle2_size = match image_generation_config.size {
Some(async_openai::types::images::ImageSize::S256x256) => Some(async_openai::types::images::ImageSize::S256x256), Some(async_openai::types::images::ImageSize::S256x256) => {
Some(async_openai::types::images::ImageSize::S512x512) => Some(async_openai::types::images::ImageSize::S512x512), Some(async_openai::types::images::ImageSize::S256x256)
Some(async_openai::types::images::ImageSize::S1024x1024) => Some(async_openai::types::images::ImageSize::S1024x1024), }
Some(async_openai::types::images::ImageSize::S512x512) => {
Some(async_openai::types::images::ImageSize::S512x512)
}
Some(async_openai::types::images::ImageSize::S1024x1024) => {
Some(async_openai::types::images::ImageSize::S1024x1024)
}
_ => None, _ => None,
}; };
@@ -401,16 +421,13 @@ impl ControllerTrait for Controller {
.map_err(|err| anyhow::anyhow!(err))?; .map_err(|err| anyhow::anyhow!(err))?;
let response_format = match model.clone() { let response_format = match model.clone() {
ImageModel::DallE2 => { ImageModel::DallE2 => Some(ImageResponseFormat::B64Json),
Some(ImageResponseFormat::B64Json) ImageModel::DallE3 => Some(ImageResponseFormat::B64Json),
}
ImageModel::DallE3 => {
Some(ImageResponseFormat::B64Json)
}
// gpt-image-1 only outputs base64 and we don't need to specify the response format. // gpt-image-1 only outputs base64 and we don't need to specify the response format.
// In fact, specifying the response format results in an error. // In fact, specifying the response format results in an error.
ImageModel::GptImage1 => None, ImageModel::GptImage1 => None,
ImageModel::GptImage1Mini => None, ImageModel::GptImage1Mini => None,
ImageModel::GptImage1dot5 => None,
ImageModel::Other(_) => Some(ImageResponseFormat::B64Json), ImageModel::Other(_) => Some(ImageResponseFormat::B64Json),
}; };
@@ -617,3 +634,17 @@ fn audio_mime_type_to_file_name(mime_type: &mxlink::mime::Mime) -> Option<String
Some(format!("audio.{}", file_extension)) Some(format!("audio.{}", file_extension))
} }
/// Returns the smallest supported size for stickers based on what the image model supports.
fn get_sticker_size(model: &ImageModel) -> async_openai::types::images::ImageSize {
use async_openai::types::images::ImageSize;
match model {
ImageModel::DallE2 => ImageSize::S256x256,
ImageModel::DallE3 => ImageSize::S1024x1024,
ImageModel::GptImage1 => ImageSize::S1024x1024,
ImageModel::GptImage1Mini => ImageSize::S1024x1024,
ImageModel::GptImage1dot5 => ImageSize::S1024x1024,
ImageModel::Other(_) => ImageSize::S1024x1024,
}
}

View File

@@ -16,16 +16,16 @@ use super::super::AgentInstantiationResult;
use super::ConfigTrait; use super::ConfigTrait;
use super::controller::ControllerType; use super::controller::ControllerType;
pub const OPENAI_IMAGE_MODEL_GPT_IMAGE_1: &str = "gpt-image-1"; pub const OPENAI_IMAGE_MODEL_GPT_IMAGE_1_DOT_5: &str = "gpt-image-1.5";
pub fn create_controller_from_yaml_value_config( pub fn create_controller_from_yaml_value_config(
agent_id: &str, agent_id: &str,
config: serde_yaml::Value, config: serde_yaml_ng::Value,
) -> AgentInstantiationResult<ControllerType> { ) -> AgentInstantiationResult<ControllerType> {
let config = match &config { let config = match &config {
serde_yaml::Value::Mapping(_) => { serde_yaml_ng::Value::Mapping(_) => {
let config: Config = let config: Config =
serde_yaml::from_value(config).map_err(AgentInstantiationError::Yaml)?; serde_yaml_ng::from_value(config).map_err(AgentInstantiationError::Yaml)?;
config config
.validate() .validate()

View File

@@ -1,12 +1,6 @@
use async_openai::types::{ use async_openai::types::responses::{
chat::{ EasyInputContent, EasyInputMessage, ImageDetail, InputContent, InputImageContent, InputItem,
ChatCompletionRequestAssistantMessageArgs, ChatCompletionRequestMessage, InputParam, MessageType, Role,
ChatCompletionRequestMessageContentPartImage,
ChatCompletionRequestSystemMessageArgs,
ChatCompletionRequestUserMessageArgs, ChatCompletionRequestUserMessageContent,
ChatCompletionRequestUserMessageContentPart,
ImageUrlArgs,
},
}; };
use crate::conversation::llm::{ use crate::conversation::llm::{
@@ -14,93 +8,41 @@ use crate::conversation::llm::{
}; };
use crate::utils::base64::base64_encode; use crate::utils::base64::base64_encode;
pub fn convert_llm_messages_to_openai_messages( pub fn convert_llm_messages_to_openai_response_input(
conversation_messages: Vec<LLMMessage>, conversation_messages: Vec<LLMMessage>,
) -> Vec<ChatCompletionRequestMessage> { ) -> InputParam {
let mut openai_conversation_messages: Vec<ChatCompletionRequestMessage> = let mut items = Vec::with_capacity(conversation_messages.len());
Vec::with_capacity(conversation_messages.len());
for message in conversation_messages { for message in conversation_messages {
let openai_message = convert_llm_message_to_openai_message(message); let role = match message.author {
if let Some(openai_message) = openai_message { LLMAuthor::Prompt => Role::System,
openai_conversation_messages.push(openai_message); LLMAuthor::Assistant => Role::Assistant,
} LLMAuthor::User => Role::User,
} };
openai_conversation_messages let content = match message.content {
} LLMMessageContent::Text(text) => EasyInputContent::Text(text),
LLMMessageContent::Image(image_details) => {
let image_url = format!(
"data:{};base64,{}",
image_details.mime,
base64_encode(&image_details.data)
);
fn convert_llm_message_to_openai_message( EasyInputContent::ContentList(vec![InputContent::InputImage(InputImageContent {
llm_message: LLMMessage, image_url: Some(image_url),
) -> Option<ChatCompletionRequestMessage> { detail: ImageDetail::Auto,
match &llm_message.content { file_id: None,
LLMMessageContent::Text(text) => Some(match llm_message.author { })])
LLMAuthor::Prompt => ChatCompletionRequestSystemMessageArgs::default()
.content(text.clone())
.build()
.expect("Failed building OpenAI system message")
.into(),
LLMAuthor::Assistant => ChatCompletionRequestAssistantMessageArgs::default()
.content(text.clone())
.build()
.expect("Failed building OpenAI assistant message")
.into(),
LLMAuthor::User => ChatCompletionRequestUserMessageArgs::default()
.content(text.clone())
.build()
.expect("Failed building OpenAI user message")
.into(),
}),
LLMMessageContent::Image(image_details) => {
let image_url = format!(
"data:{};base64,{}",
image_details.mime,
base64_encode(&image_details.data)
);
let part = ChatCompletionRequestUserMessageContentPart::ImageUrl(
ChatCompletionRequestMessageContentPartImage {
image_url: ImageUrlArgs::default()
.url(image_url)
.build()
.expect("Failed building OpenAI image url"),
},
);
let message_content = ChatCompletionRequestUserMessageContent::Array(vec![part]);
match llm_message.author {
LLMAuthor::User => Some(
ChatCompletionRequestUserMessageArgs::default()
.content(message_content)
.build()
.expect("Failed building OpenAI user message")
.into(),
),
_ => {
tracing::warn!(
"OpenAI API does not support image content for messages authored by {:?}. This message part will be skipped.",
llm_message.author
);
None
}
} }
} };
}
}
pub(super) fn convert_string_to_enum<T>(value: &str) -> Result<T, String> items.push(InputItem::EasyMessage(EasyInputMessage {
where r#type: MessageType::Message,
T: serde::de::DeserializeOwned, role,
{ content,
// This is a hacky way to construct an enum from the string we have. }));
let enum_result: serde_json::Result<T> = serde_json::from_str(&format!("\"{}\"", value));
match enum_result {
Ok(enum_result) => Ok(enum_result),
Err(err) => {
tracing::debug!(?err, "Failed to parse into enum");
Err(format!("The value ({}) is not supported.", value))
}
} }
InputParam::Items(items)
} }

View File

@@ -95,6 +95,7 @@ impl TryInto<OpenAITextGenerationConfig> for TextGenerationConfig {
max_response_tokens: self.max_response_tokens, max_response_tokens: self.max_response_tokens,
max_completion_tokens: None, max_completion_tokens: None,
max_context_tokens: self.max_context_tokens, max_context_tokens: self.max_context_tokens,
tools: Default::default(),
}) })
} }
} }
@@ -161,13 +162,14 @@ impl TryInto<OpenAITextToSpeechConfig> for TextToSpeechConfig {
type Error = String; type Error = String;
fn try_into(self) -> Result<OpenAITextToSpeechConfig, Self::Error> { fn try_into(self) -> Result<OpenAITextToSpeechConfig, Self::Error> {
let model_id = convert_string_to_enum::<async_openai::types::audio::SpeechModel>(&self.model_id)?; let model_id =
convert_string_to_enum::<async_openai::types::audio::SpeechModel>(&self.model_id)?;
let voice = convert_string_to_enum::<async_openai::types::audio::Voice>(&self.voice)?; let voice = convert_string_to_enum::<async_openai::types::audio::Voice>(&self.voice)?;
let response_format = convert_string_to_enum::<async_openai::types::audio::SpeechResponseFormat>( let response_format = convert_string_to_enum::<
&self.response_format, async_openai::types::audio::SpeechResponseFormat,
)?; >(&self.response_format)?;
Ok(OpenAITextToSpeechConfig { Ok(OpenAITextToSpeechConfig {
model_id, model_id,
@@ -224,25 +226,25 @@ impl TryInto<OpenAIImageGenerationConfig> for ImageGenerationConfig {
fn try_into(self) -> Result<OpenAIImageGenerationConfig, Self::Error> { fn try_into(self) -> Result<OpenAIImageGenerationConfig, Self::Error> {
let size = if let Some(size) = &self.size { let size = if let Some(size) = &self.size {
Some(convert_string_to_enum::<async_openai::types::images::ImageSize>( Some(convert_string_to_enum::<
size, async_openai::types::images::ImageSize,
)?) >(size)?)
} else { } else {
None None
}; };
let style = if let Some(style) = &self.style { let style = if let Some(style) = &self.style {
Some(convert_string_to_enum::<async_openai::types::images::ImageStyle>( Some(convert_string_to_enum::<
style, async_openai::types::images::ImageStyle,
)?) >(style)?)
} else { } else {
None None
}; };
let quality = if let Some(quality) = &self.quality { let quality = if let Some(quality) = &self.quality {
Some(convert_string_to_enum::<async_openai::types::images::ImageQuality>( Some(convert_string_to_enum::<
quality, async_openai::types::images::ImageQuality,
)?) >(quality)?)
} else { } else {
None None
}; };

View File

@@ -3,6 +3,8 @@ use etke_openai_api_rust::chat::{ChatApi, ChatBody};
use etke_openai_api_rust::images::{ImagesApi, ImagesBody}; use etke_openai_api_rust::images::{ImagesApi, ImagesBody};
use etke_openai_api_rust::{Auth, Message, OpenAI}; use etke_openai_api_rust::{Auth, Message, OpenAI};
const SMALLEST_IMAGE_SIZE: &str = "256x256";
use super::super::ControllerTrait; use super::super::ControllerTrait;
use crate::utils::base64::base64_decode; use crate::utils::base64::base64_decode;
use crate::{ use crate::{
@@ -303,9 +305,11 @@ impl ControllerTrait for Controller {
// when they span multiple lines. // when they span multiple lines.
let prompt = prompt.replace("\n", " "); let prompt = prompt.replace("\n", " ");
let size: Option<String> = params let size: Option<String> = if params.smallest_size_possible {
.size_override Some(SMALLEST_IMAGE_SIZE.to_owned())
.or_else(|| image_generation_config.size.clone()); } else {
image_generation_config.size.clone()
};
let request = ImagesBody { let request = ImagesBody {
model: Some(image_generation_config.model_id.to_owned()), model: Some(image_generation_config.model_id.to_owned()),

View File

@@ -26,12 +26,12 @@ use super::controller::ControllerType;
pub fn create_controller_from_yaml_value_config( pub fn create_controller_from_yaml_value_config(
agent_id: &str, agent_id: &str,
config: serde_yaml::Value, config: serde_yaml_ng::Value,
) -> AgentInstantiationResult<ControllerType> { ) -> AgentInstantiationResult<ControllerType> {
let config = match &config { let config = match &config {
serde_yaml::Value::Mapping(_) => { serde_yaml_ng::Value::Mapping(_) => {
let config: Config = let config: Config =
serde_yaml::from_value(config).map_err(AgentInstantiationError::Yaml)?; serde_yaml_ng::from_value(config).map_err(AgentInstantiationError::Yaml)?;
config config
.validate() .validate()

View File

@@ -1,12 +1,13 @@
use std::fs;
use std::sync::Arc; use std::sync::Arc;
use std::{future::Future, pin::Pin}; use std::{future::Future, pin::Pin};
use mxlink::matrix_sdk::Room; use mxlink::matrix_sdk::Room;
use mxlink::matrix_sdk::media::{MediaFormat, MediaRequestParameters}; use mxlink::matrix_sdk::media::{MediaFormat, MediaRequestParameters};
use mxlink::matrix_sdk::ruma::api::client::profile::{AvatarUrl, DisplayName};
use mxlink::matrix_sdk::ruma::{ use mxlink::matrix_sdk::ruma::{
MilliSecondsSinceUnixEpoch, OwnedUserId, events::room::MediaSource, MilliSecondsSinceUnixEpoch, OwnedUserId, events::room::MediaSource,
}; };
use mxlink::matrix_sdk::ruma::api::client::profile::{AvatarUrl, DisplayName};
use mxlink::{ use mxlink::{
InitConfig, LoginConfig, LoginCredentials, LoginEncryption, MatrixLink, PersistenceConfig, InitConfig, LoginConfig, LoginCredentials, LoginEncryption, MatrixLink, PersistenceConfig,
@@ -18,12 +19,13 @@ use mxlink::helpers::account_data_config::{
RoomConfigManager as AccountDataRoomConfigManager, RoomConfigManager as AccountDataRoomConfigManager,
}; };
use mxlink::helpers::encryption::Manager as EncryptionManager; use mxlink::helpers::encryption::Manager as EncryptionManager;
use mxlink::mime::Mime;
use crate::agent::Manager as AgentManager; use crate::agent::Manager as AgentManager;
use crate::entity::catch_up_marker::{ use crate::entity::catch_up_marker::{
CatchUpMarker, CatchUpMarkerManager, DelayedCatchUpMarkerManager, CatchUpMarker, CatchUpMarkerManager, DelayedCatchUpMarkerManager,
}; };
use crate::entity::cfg::Config; use crate::entity::cfg::{Avatar, Config, ConfigUserAuth};
use crate::entity::globalconfig::{GlobalConfig, GlobalConfigurationManager}; use crate::entity::globalconfig::{GlobalConfig, GlobalConfigurationManager};
use crate::entity::roomconfig::{RoomConfig, RoomConfigurationManager}; use crate::entity::roomconfig::{RoomConfig, RoomConfigurationManager};
@@ -316,34 +318,72 @@ impl Bot {
} }
} }
let should_update_avatar = match &current_avatar_url { let desired_avatar: Option<(Vec<u8>, Mime)> = match &self.inner.config.user.avatar {
Some(avatar_url) => { Avatar::Keep => {
let request = MediaRequestParameters { tracing::info!("Avatar configured to keep current, skipping avatar management");
source: MediaSource::Plain(avatar_url.to_owned()), None
format: MediaFormat::File, }
}; Avatar::Default => {
tracing::info!("Avatar configured to use default");
let content = media Some((
.get_media_content(&request, true) LOGO_BYTES.to_vec(),
.await LOGO_MIME_TYPE
.map_err(|e| anyhow::anyhow!("Failed fetching existing avatar: {:?}", e))?; .parse()
.expect("Failed parsing mime type for logo"),
content.as_slice() != LOGO_BYTES ))
}
Avatar::Custom(avatar_path) => {
tracing::info!(?avatar_path, "Avatar configured to use custom path");
let bytes = fs::read(avatar_path).map_err(|e| {
anyhow::anyhow!("Failed reading avatar from {:?}: {:?}", avatar_path, e)
})?;
let mime = mime_guess::from_path(avatar_path).first_or_octet_stream();
tracing::debug!(?mime, bytes_len = bytes.len(), "Loaded custom avatar");
Some((bytes, mime))
} }
None => true,
}; };
if should_update_avatar { if let Some((desired_bytes, mime_type)) = desired_avatar {
tracing::info!("Updating avatar.."); let should_update_avatar = match &current_avatar_url {
Some(avatar_url) => {
tracing::debug!(?avatar_url, "Fetching current avatar to compare");
let request = MediaRequestParameters {
source: MediaSource::Plain(avatar_url.to_owned()),
format: MediaFormat::File,
};
let mime_type = LOGO_MIME_TYPE let content = media
.parse() .get_media_content(&request, true)
.expect("Failed parsing mime type for logo"); .await
.map_err(|e| anyhow::anyhow!("Failed fetching existing avatar: {:?}", e))?;
account let needs_update = content.as_slice() != desired_bytes;
.upload_avatar(&mime_type, LOGO_BYTES.to_vec())
.await tracing::debug!(
.map_err(|e| anyhow::anyhow!("Failed uploading avatar: {:?}", e))?; current_bytes_len = content.len(),
desired_bytes_len = desired_bytes.len(),
?needs_update,
"Compared current and desired avatar"
);
needs_update
}
None => {
tracing::debug!("No current avatar set, will upload");
true
}
};
if should_update_avatar {
tracing::info!("Updating avatar..");
account
.upload_avatar(&mime_type, desired_bytes)
.await
.map_err(|e| anyhow::anyhow!("Failed uploading avatar: {:?}", e))?;
tracing::info!("Avatar updated successfully");
} else {
tracing::debug!("Avatar already up to date, skipping upload");
}
} }
Ok(()) Ok(())
@@ -355,10 +395,22 @@ async fn create_matrix_link(config: &Config) -> anyhow::Result<MatrixLink> {
let session_encryption_key = config.persistence.session_encryption_key()?; let session_encryption_key = config.persistence.session_encryption_key()?;
let db_dir_path: std::path::PathBuf = config.persistence.db_dir_path()?; let db_dir_path: std::path::PathBuf = config.persistence.db_dir_path()?;
let login_creds = LoginCredentials::UserPassword( let user_auth = config.user.auth_config(&config.homeserver.server_name)?;
config.user.mxid_localpart.to_owned(),
config.user.password.to_owned(), let login_creds = match user_auth {
); ConfigUserAuth::UserPassword { username, password } => {
LoginCredentials::UserPassword(username, password)
}
ConfigUserAuth::AccessToken {
user_id,
device_id,
access_token,
} => LoginCredentials::AccessToken {
user_id,
device_id,
access_token,
},
};
let login_encryption = LoginEncryption::new( let login_encryption = LoginEncryption::new(
config.user.encryption.recovery_passphrase.clone(), config.user.encryption.recovery_passphrase.clone(),

View File

@@ -5,7 +5,7 @@ use anyhow::anyhow;
use crate::agent::AgentPurpose; use crate::agent::AgentPurpose;
pub use crate::entity::cfg::{Config, defaults as cfg_defaults, env as cfg_env}; pub use crate::entity::cfg::{Avatar, Config, defaults as cfg_defaults, env as cfg_env};
pub fn load() -> anyhow::Result<Config> { pub fn load() -> anyhow::Result<Config> {
let config_file_path = env::var(cfg_env::BAIBOT_CONFIG_FILE_PATH) let config_file_path = env::var(cfg_env::BAIBOT_CONFIG_FILE_PATH)
@@ -21,7 +21,7 @@ pub fn load() -> anyhow::Result<Config> {
} }
let config_str = std::fs::read_to_string(config_file_path)?; let config_str = std::fs::read_to_string(config_file_path)?;
let mut config: Config = serde_yaml::from_str(&config_str)?; let mut config: Config = serde_yaml_ng::from_str(&config_str)?;
// Allow environment variables to override some configuration keys // Allow environment variables to override some configuration keys
for (key, value) in env::vars() { for (key, value) in env::vars() {
@@ -29,7 +29,15 @@ pub fn load() -> anyhow::Result<Config> {
cfg_env::BAIBOT_HOMESERVER_SERVER_NAME => config.homeserver.server_name = value, cfg_env::BAIBOT_HOMESERVER_SERVER_NAME => config.homeserver.server_name = value,
cfg_env::BAIBOT_HOMESERVER_URL => config.homeserver.url = value, cfg_env::BAIBOT_HOMESERVER_URL => config.homeserver.url = value,
cfg_env::BAIBOT_USER_MXID_LOCALPART => config.user.mxid_localpart = value, cfg_env::BAIBOT_USER_MXID_LOCALPART => config.user.mxid_localpart = value,
cfg_env::BAIBOT_USER_PASSWORD => config.user.password = value, cfg_env::BAIBOT_USER_PASSWORD => {
config.user.password = optional_non_empty(value);
}
cfg_env::BAIBOT_USER_ACCESS_TOKEN => {
config.user.access_token = optional_non_empty(value);
}
cfg_env::BAIBOT_USER_DEVICE_ID => {
config.user.device_id = optional_non_empty(value);
}
cfg_env::BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE => { cfg_env::BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE => {
config.user.encryption.recovery_passphrase = Some(value); config.user.encryption.recovery_passphrase = Some(value);
} }
@@ -37,6 +45,9 @@ pub fn load() -> anyhow::Result<Config> {
config.user.encryption.recovery_reset_allowed = value.parse::<bool>()?; config.user.encryption.recovery_reset_allowed = value.parse::<bool>()?;
} }
cfg_env::BAIBOT_USER_NAME => config.user.name = value, cfg_env::BAIBOT_USER_NAME => config.user.name = value,
cfg_env::BAIBOT_USER_AVATAR => {
config.user.avatar = Avatar::from_string(value);
}
cfg_env::BAIBOT_COMMAND_PREFIX => config.command_prefix = value, cfg_env::BAIBOT_COMMAND_PREFIX => config.command_prefix = value,
cfg_env::BAIBOT_ROOM_POST_JOIN_SELF_INTRODUCTION_ENABLED => { cfg_env::BAIBOT_ROOM_POST_JOIN_SELF_INTRODUCTION_ENABLED => {
config.room.post_join_self_introduction_enabled = value.parse::<bool>()?; config.room.post_join_self_introduction_enabled = value.parse::<bool>()?;
@@ -117,3 +128,7 @@ pub fn load() -> anyhow::Result<Config> {
Ok(config) Ok(config)
} }
fn optional_non_empty(value: String) -> Option<String> {
if value.is_empty() { None } else { Some(value) }
}

View File

@@ -28,18 +28,18 @@ pub async fn handle_set(
message_context: &MessageContext, message_context: &MessageContext,
patterns: &Option<Vec<String>>, patterns: &Option<Vec<String>>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
if let Some(patterns) = patterns { if let Some(patterns) = patterns
if let Err(err) = mxidwc::parse_patterns_vector(patterns) { && let Err(err) = mxidwc::parse_patterns_vector(patterns)
bot.messaging() {
.send_error_markdown_no_fail( bot.messaging()
message_context.room(), .send_error_markdown_no_fail(
&strings::access::failed_to_parse_patterns(&err.to_string()), message_context.room(),
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()), &strings::access::failed_to_parse_patterns(&err.to_string()),
) MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()),
.await; )
.await;
return Ok(()); return Ok(());
}
} }
let mut global_config_manager_guard = bot.global_config_manager().lock().await; let mut global_config_manager_guard = bot.global_config_manager().lock().await;

View File

@@ -24,18 +24,18 @@ pub async fn handle_set(
message_context: &MessageContext, message_context: &MessageContext,
patterns: &Option<Vec<String>>, patterns: &Option<Vec<String>>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
if let Some(patterns) = patterns { if let Some(patterns) = patterns
if let Err(err) = mxidwc::parse_patterns_vector(patterns) { && let Err(err) = mxidwc::parse_patterns_vector(patterns)
bot.messaging() {
.send_error_markdown_no_fail( bot.messaging()
message_context.room(), .send_error_markdown_no_fail(
&strings::access::failed_to_parse_patterns(&err.to_string()), message_context.room(),
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()), &strings::access::failed_to_parse_patterns(&err.to_string()),
) MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()),
.await; )
.await;
return Ok(()); return Ok(());
}
} }
let mut global_config_manager_guard = bot.global_config_manager().lock().await; let mut global_config_manager_guard = bot.global_config_manager().lock().await;

View File

@@ -15,7 +15,7 @@ use crate::{Bot, entity::MessageContext};
struct ParsedAgentConfig { struct ParsedAgentConfig {
agent: AgentInstance, agent: AgentInstance,
config: serde_yaml::Value, config: serde_yaml_ng::Value,
} }
pub async fn handle_room_local( pub async fn handle_room_local(
@@ -250,7 +250,7 @@ async fn send_guide(
provider: &AgentProvider, provider: &AgentProvider,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let sample_config = crate::agent::default_config_for_provider(provider); let sample_config = crate::agent::default_config_for_provider(provider);
let sample_config_pretty_yaml = serde_yaml::to_string(&sample_config)?; let sample_config_pretty_yaml = serde_yaml_ng::to_string(&sample_config)?;
bot.messaging() bot.messaging()
.send_text_markdown_no_fail( .send_text_markdown_no_fail(
@@ -263,7 +263,7 @@ async fn send_guide(
Ok(()) Ok(())
} }
fn parse_from_message_to_yaml_value(text: &str) -> Result<serde_yaml::Value, String> { fn parse_from_message_to_yaml_value(text: &str) -> Result<serde_yaml_ng::Value, String> {
let mut text = text.trim(); let mut text = text.trim();
if text.starts_with("```") { if text.starts_with("```") {
@@ -274,10 +274,10 @@ fn parse_from_message_to_yaml_value(text: &str) -> Result<serde_yaml::Value, Str
text = text.trim_end_matches("```"); text = text.trim_end_matches("```");
} }
let config: serde_yaml::Value = serde_yaml::from_str(text).map_err(|e| e.to_string())?; let config: serde_yaml_ng::Value = serde_yaml_ng::from_str(text).map_err(|e| e.to_string())?;
match config { match config {
serde_yaml::Value::Mapping(_) => {} serde_yaml_ng::Value::Mapping(_) => {}
_ => { _ => {
return Err("Not a valid YAML hashmap".to_owned()); return Err("Not a valid YAML hashmap".to_owned());
} }

View File

@@ -2,12 +2,12 @@
fn agent_config_parsing_works() { fn agent_config_parsing_works() {
struct TestCase { struct TestCase {
input: String, input: String,
expected: Option<serde_yaml::Value>, expected: Option<serde_yaml_ng::Value>,
} }
let provider = crate::agent::AgentProvider::OpenAI; let provider = crate::agent::AgentProvider::OpenAI;
let sample_config = crate::agent::default_config_for_provider(&provider); let sample_config = crate::agent::default_config_for_provider(&provider);
let sample_config_pretty_yaml = serde_yaml::to_string(&sample_config).unwrap(); let sample_config_pretty_yaml = serde_yaml_ng::to_string(&sample_config).unwrap();
let test_cases = vec![ let test_cases = vec![
// Invalid input // Invalid input

View File

@@ -64,7 +64,7 @@ pub async fn handle(
PublicIdentifier::Static(_) => {} PublicIdentifier::Static(_) => {}
}; };
let config_yaml_pretty = serde_yaml::to_string(&agent.definition().config)?; let config_yaml_pretty = serde_yaml_ng::to_string(&agent.definition().config)?;
bot.messaging() bot.messaging()
.send_text_markdown_no_fail( .send_text_markdown_no_fail(

View File

@@ -39,18 +39,18 @@ async fn dispatch_config_related_handler(
message_context: &MessageContext, message_context: &MessageContext,
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() { && !message_context.sender_can_manage_global_config()
bot.messaging() {
.send_error_markdown_no_fail( bot.messaging()
message_context.room(), .send_error_markdown_no_fail(
strings::global_config::no_permissions_to_administrate(), message_context.room(),
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()), strings::global_config::no_permissions_to_administrate(),
) MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()),
.await; )
return Ok(()); .await;
} return Ok(());
}; }
let room_settings = match config_type { let room_settings = match config_type {
SettingsStorageSource::Room => &message_context.room_config().settings, SettingsStorageSource::Room => &message_context.room_config().settings,

View File

@@ -12,9 +12,6 @@ use crate::strings;
use crate::utils::mime::get_file_extension; use crate::utils::mime::get_file_extension;
use crate::{Bot, entity::MessageContext}; use crate::{Bot, entity::MessageContext};
// We may make this configurable (per room, etc.) in the future, but for now it's hardcoded.
const STICKER_SIZE: &str = "256x256";
pub async fn handle_image( pub async fn handle_image(
bot: &Bot, bot: &Bot,
matrix_link: MatrixLink, matrix_link: MatrixLink,
@@ -177,7 +174,7 @@ pub async fn handle_sticker(
); );
let params = ImageGenerationParams::default() let params = ImageGenerationParams::default()
.with_size_override(Some(STICKER_SIZE.to_owned())) .with_smallest_size_possible(true)
.with_cheaper_model_switching_allowed(true) .with_cheaper_model_switching_allowed(true)
.with_cheaper_quality_switching_allowed(true); .with_cheaper_quality_switching_allowed(true);

View File

@@ -154,35 +154,35 @@ pub async fn process_matrix_messages(
let mut message = message.clone(); let mut message = message.clone();
if i == 0 && !params.first_message_prefixes_to_strip.is_empty() { if i == 0
if let MatrixMessageContent::Text(message_text) = &message.content { && !params.first_message_prefixes_to_strip.is_empty()
let mut message_text = message_text.clone(); && let MatrixMessageContent::Text(message_text) = &message.content
{
let mut message_text = message_text.clone();
for prefix in &params.first_message_prefixes_to_strip { for prefix in &params.first_message_prefixes_to_strip {
if let Some(message_text_stripped) = message_text.strip_prefix(prefix) { if let Some(message_text_stripped) = message_text.strip_prefix(prefix) {
message_text = message_text_stripped.to_owned(); message_text = message_text_stripped.to_owned();
}
} }
message.content = MatrixMessageContent::Text(message_text.trim().to_owned());
} }
message.content = MatrixMessageContent::Text(message_text.trim().to_owned());
} }
// We only strip `bot_user_prefixes_to_strip`-defined prefixes from messages that mention the bot user. // We only strip `bot_user_prefixes_to_strip`-defined prefixes from messages that mention the bot user.
if !params.bot_user_prefixes_to_strip.is_empty() if !params.bot_user_prefixes_to_strip.is_empty()
&& message.mentioned_users.contains(&params.bot_user_id) && message.mentioned_users.contains(&params.bot_user_id)
&& let MatrixMessageContent::Text(message_text) = &message.content
{ {
if let MatrixMessageContent::Text(message_text) = &message.content { let mut message_text = message_text.clone();
let mut message_text = message_text.clone();
for prefix in &params.bot_user_prefixes_to_strip { for prefix in &params.bot_user_prefixes_to_strip {
if let Some(message_text_stripped) = message_text.strip_prefix(prefix) { if let Some(message_text_stripped) = message_text.strip_prefix(prefix) {
message_text = message_text_stripped.to_owned(); message_text = message_text_stripped.to_owned();
}
} }
message.content = MatrixMessageContent::Text(message_text.trim().to_owned());
} }
message.content = MatrixMessageContent::Text(message_text.trim().to_owned());
} }
messages_filtered.push(message); messages_filtered.push(message);

View File

@@ -1,7 +1,8 @@
use std::path::PathBuf; use std::path::PathBuf;
use mxlink::helpers::encryption::EncryptionKey; use mxlink::helpers::encryption::EncryptionKey;
use serde::{Deserialize, Serialize}; use mxlink::matrix_sdk::ruma::{OwnedDeviceId, OwnedUserId};
use serde::{Deserialize, Deserializer, Serialize};
use crate::{ use crate::{
agent::{AgentDefinition, AgentPurpose, PublicIdentifier}, agent::{AgentDefinition, AgentPurpose, PublicIdentifier},
@@ -38,7 +39,7 @@ pub struct Config {
impl Config { impl Config {
pub fn validate(&self) -> anyhow::Result<()> { pub fn validate(&self) -> anyhow::Result<()> {
self.homeserver.validate()?; self.homeserver.validate()?;
self.user.validate()?; self.user.validate(&self.homeserver.server_name)?;
self.persistence.validate()?; self.persistence.validate()?;
self.room.validate()?; self.room.validate()?;
self.access.validate()?; self.access.validate()?;
@@ -57,6 +58,19 @@ impl Config {
} }
} }
#[derive(Debug)]
pub enum ConfigUserAuth {
UserPassword {
username: String,
password: String,
},
AccessToken {
user_id: OwnedUserId,
device_id: OwnedDeviceId,
access_token: String,
},
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct ConfigHomeserver { pub struct ConfigHomeserver {
pub server_name: String, pub server_name: String,
@@ -83,20 +97,72 @@ impl ConfigHomeserver {
} }
} }
/// Configuration for the bot's avatar.
///
/// - `Default`: Use the built-in default avatar (null, empty string, or missing in config)
/// - `Keep`: Don't touch the avatar, keep whatever is already set ("keep" in config)
/// - `Custom(String)`: Use a custom avatar from the specified file path
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub enum Avatar {
/// Use the built-in default avatar
#[default]
Default,
/// Keep the current avatar, don't change it
Keep,
/// Use a custom avatar from the specified file path
Custom(String),
}
impl<'de> Deserialize<'de> for Avatar {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value: Option<String> = Option::deserialize(deserializer)?;
Ok(match value {
None => Avatar::Default,
Some(s) => Avatar::from_string(s),
})
}
}
impl Avatar {
pub fn from_string(value: String) -> Self {
if value.is_empty() {
Avatar::Default
} else if value.eq_ignore_ascii_case("keep") {
Avatar::Keep
} else {
Avatar::Custom(value)
}
}
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct ConfigUser { pub struct ConfigUser {
pub mxid_localpart: String, pub mxid_localpart: String,
pub password: String,
#[serde(default)]
pub password: Option<String>,
#[serde(default)]
pub access_token: Option<String>,
#[serde(default)]
pub device_id: Option<String>,
#[serde(default = "super::defaults::name")] #[serde(default = "super::defaults::name")]
pub name: String, pub name: String,
#[serde(default)] #[serde(default)]
pub encryption: ConfigUserEncryption, pub encryption: ConfigUserEncryption,
#[serde(default)]
pub avatar: Avatar,
} }
impl ConfigUser { impl ConfigUser {
pub fn validate(&self) -> anyhow::Result<()> { pub fn validate(&self, homeserver_server_name: &str) -> anyhow::Result<()> {
if self.mxid_localpart.is_empty() { if self.mxid_localpart.is_empty() {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"The user.mxid_localpart ({}) configuration must be set", "The user.mxid_localpart ({}) configuration must be set",
@@ -104,12 +170,7 @@ impl ConfigUser {
)); ));
} }
if self.password.is_empty() { self.auth_config(homeserver_server_name)?;
return Err(anyhow::anyhow!(
"The user.password ({}) configuration must be set",
super::env::BAIBOT_USER_PASSWORD
));
}
if self.name.is_empty() { if self.name.is_empty() {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
@@ -122,6 +183,57 @@ impl ConfigUser {
Ok(()) Ok(())
} }
pub fn auth_config(&self, homeserver_server_name: &str) -> anyhow::Result<ConfigUserAuth> {
let password = self.password.as_deref().filter(|value| !value.is_empty());
let access_token = self
.access_token
.as_deref()
.filter(|value| !value.is_empty());
match (password, access_token) {
(Some(_), Some(_)) => Err(anyhow::anyhow!(
"Set exactly one authentication method: either user.password ({}) OR user.access_token ({}) + user.device_id ({})",
super::env::BAIBOT_USER_PASSWORD,
super::env::BAIBOT_USER_ACCESS_TOKEN,
super::env::BAIBOT_USER_DEVICE_ID
)),
(None, None) => Err(anyhow::anyhow!(
"Set one authentication method: either user.password ({}) OR user.access_token ({}) + user.device_id ({})",
super::env::BAIBOT_USER_PASSWORD,
super::env::BAIBOT_USER_ACCESS_TOKEN,
super::env::BAIBOT_USER_DEVICE_ID
)),
(Some(password), None) => Ok(ConfigUserAuth::UserPassword {
username: self.mxid_localpart.to_owned(),
password: password.to_owned(),
}),
(None, Some(access_token)) => {
let device_id = self
.device_id
.as_deref()
.filter(|value| !value.is_empty())
.ok_or_else(|| {
anyhow::anyhow!(
"user.device_id ({}) must be set when using access token authentication",
super::env::BAIBOT_USER_DEVICE_ID
)
})?;
let user_id = OwnedUserId::try_from(format!(
"@{}:{}",
self.mxid_localpart, homeserver_server_name
))
.map_err(|e| anyhow::anyhow!("Invalid user ID: {e}"))?;
Ok(ConfigUserAuth::AccessToken {
user_id,
device_id: OwnedDeviceId::from(device_id),
access_token: access_token.to_owned(),
})
}
}
}
} }
#[derive(Debug, Default, Serialize, Deserialize)] #[derive(Debug, Default, Serialize, Deserialize)]
@@ -132,13 +244,13 @@ pub struct ConfigUserEncryption {
impl ConfigUserEncryption { impl ConfigUserEncryption {
pub fn validate(&self) -> anyhow::Result<()> { pub fn validate(&self) -> anyhow::Result<()> {
if let Some(passphrase) = &self.recovery_passphrase { if let Some(passphrase) = &self.recovery_passphrase
if passphrase.is_empty() { && passphrase.is_empty()
return Err(anyhow::anyhow!( {
"The user.encryption.recovery_passphrase ({}) configuration must either be null or set to a non-empty passphrase", return Err(anyhow::anyhow!(
super::env::BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE "The user.encryption.recovery_passphrase ({}) configuration must either be null or set to a non-empty passphrase",
)); super::env::BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE
} ));
} }
Ok(()) Ok(())
@@ -424,3 +536,7 @@ impl TryInto<GlobalConfig> for ConfigInitialGlobalConfig {
Ok(entity) Ok(entity)
} }
} }
#[cfg(test)]
#[path = "config_tests.rs"]
mod config_tests;

View File

@@ -0,0 +1,117 @@
use super::{Avatar, ConfigUser, ConfigUserAuth, ConfigUserEncryption};
use crate::entity::cfg::env;
fn base_user() -> ConfigUser {
ConfigUser {
mxid_localpart: "baibot".to_owned(),
password: None,
access_token: None,
device_id: None,
name: "baibot".to_owned(),
encryption: ConfigUserEncryption {
recovery_passphrase: None,
recovery_reset_allowed: false,
},
avatar: Avatar::Default,
}
}
#[test]
fn auth_config_uses_password_mode() {
let mut user = base_user();
user.password = Some("secret".to_owned());
let auth = user
.auth_config("example.com")
.expect("password auth should be valid");
match auth {
ConfigUserAuth::UserPassword { username, password } => {
assert_eq!(username, "baibot");
assert_eq!(password, "secret");
}
ConfigUserAuth::AccessToken { .. } => {
panic!("expected password auth mode");
}
}
}
#[test]
fn auth_config_uses_access_token_mode() {
let mut user = base_user();
user.access_token = Some("token123".to_owned());
user.device_id = Some("DEVICE1".to_owned());
let auth = user
.auth_config("example.com")
.expect("access token auth should be valid");
match auth {
ConfigUserAuth::AccessToken {
user_id,
device_id,
access_token,
} => {
assert_eq!(user_id.as_str(), "@baibot:example.com");
assert_eq!(device_id.as_str(), "DEVICE1");
assert_eq!(access_token, "token123");
}
ConfigUserAuth::UserPassword { .. } => {
panic!("expected access token auth mode");
}
}
}
#[test]
fn auth_config_rejects_both_auth_methods() {
let mut user = base_user();
user.password = Some("secret".to_owned());
user.access_token = Some("token123".to_owned());
user.device_id = Some("DEVICE1".to_owned());
let err = user
.auth_config("example.com")
.expect_err("both auth methods should be rejected");
assert!(
err.to_string()
.contains("exactly one authentication method")
);
}
#[test]
fn auth_config_rejects_missing_auth() {
let user = base_user();
let err = user
.auth_config("example.com")
.expect_err("missing auth should be rejected");
assert!(err.to_string().contains("Set one authentication method"));
}
#[test]
fn auth_config_rejects_access_token_without_device_id() {
let mut user = base_user();
user.access_token = Some("token123".to_owned());
let err = user
.auth_config("example.com")
.expect_err("access token mode without device_id should be rejected");
assert!(err.to_string().contains(env::BAIBOT_USER_DEVICE_ID));
}
#[test]
fn auth_config_treats_empty_strings_as_unset() {
let mut user = base_user();
user.password = Some(String::new());
user.access_token = Some(String::new());
user.device_id = Some(String::new());
let err = user
.auth_config("example.com")
.expect_err("empty auth values should be treated as unset");
assert!(err.to_string().contains("Set one authentication method"));
}

View File

@@ -5,7 +5,10 @@ pub const BAIBOT_HOMESERVER_URL: &str = "BAIBOT_HOMESERVER_URL";
pub const BAIBOT_USER_MXID_LOCALPART: &str = "BAIBOT_USER_MXID_LOCALPART"; pub const BAIBOT_USER_MXID_LOCALPART: &str = "BAIBOT_USER_MXID_LOCALPART";
pub const BAIBOT_USER_PASSWORD: &str = "BAIBOT_USER_PASSWORD"; pub const BAIBOT_USER_PASSWORD: &str = "BAIBOT_USER_PASSWORD";
pub const BAIBOT_USER_ACCESS_TOKEN: &str = "BAIBOT_USER_ACCESS_TOKEN";
pub const BAIBOT_USER_DEVICE_ID: &str = "BAIBOT_USER_DEVICE_ID";
pub const BAIBOT_USER_NAME: &str = "BAIBOT_USER_NAME"; pub const BAIBOT_USER_NAME: &str = "BAIBOT_USER_NAME";
pub const BAIBOT_USER_AVATAR: &str = "BAIBOT_USER_AVATAR";
pub const BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE: &str = pub const BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE: &str =
"BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE"; "BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE";
pub const BAIBOT_USER_ENCRYPTION_RECOVERY_RESET_ALLOWED: &str = pub const BAIBOT_USER_ENCRYPTION_RECOVERY_RESET_ALLOWED: &str =

View File

@@ -2,4 +2,4 @@ mod config;
pub mod defaults; pub mod defaults;
pub mod env; pub mod env;
pub use config::Config; pub use config::{Avatar, Config, ConfigUserAuth};

View File

@@ -109,11 +109,21 @@ pub fn help_provider_details(id: &str, info: &AgentProviderInfo) -> String {
let mut purpose_line = format!("{} {}", purpose.emoji(), purpose.as_str()); let mut purpose_line = format!("{} {}", purpose.emoji(), purpose.as_str());
if let AgentPurpose::TextGeneration = purpose { if let AgentPurpose::TextGeneration = purpose {
let mut extras = vec![];
if info.text_generation_supports_vision { if info.text_generation_supports_vision {
purpose_line = format!("{} ({})", purpose_line, "incl. vision"); extras.push("incl. vision");
} else { } else {
purpose_line = format!("{} ({})", purpose_line, "no vision"); extras.push("no vision");
} }
if info.text_generation_supports_tools {
extras.push("incl. tools");
} else {
extras.push("no tools");
}
purpose_line = format!("{} ({})", purpose_line, extras.join(", "));
} }
capabilities.push(purpose_line); capabilities.push(purpose_line);

View File

@@ -64,7 +64,7 @@ To create a sticker, send a command like `%command_prefix% sticker A huge bowl o
The difference from **creating images** is that the bot will: The difference from **creating images** is that the bot will:
- create a smaller-resolution image (`256x256`) - smaller/quicker, but still good enough for a sticker - create a smaller-resolution image (as small as the model allows) - smaller/quicker, but still good enough for a sticker
- potentially switch to a different (cheaper or otherwise more suitable) model, if available - potentially switch to a different (cheaper or otherwise more suitable) model, if available
- post the image directly to the room (as a reply to your message), without starting a threaded conversation - post the image directly to the room (as a reply to your message), without starting a threaded conversation
"#; "#;