Compare commits

..
Author SHA1 Message Date
Penar Musaraj 059da39a4a DEV: output raw response in dev mode
Makes it easier to debug issues, for example if rate limits are hit.
2025-05-13 16:54:10 -04:00
Discourse Translator Bot 22ccf2968f Update translations (#1335) 2025-05-13 09:22:01 -04:00
Roman Rizzi aef84bc5bb FEATURE: Examples support for personas. (#1334)
Examples simulate previous interactions with an LLM and come
right after the system prompt. This helps grounding the model and
producing better responses.
2025-05-13 10:06:16 -03:00
Keegan George acd1986a5c FEATURE: Improved diff streaming for AI composer helper (#1332)
This update improves the animation for streaming a diff of changes when AI helper proofread is triggered. It shows the original text with diff changes live instead of after the fact.
2025-05-12 15:57:59 -07:00
dependabot[bot] 100953319e Build(deps-dev): Bump rack from 3.1.12 to 3.1.14 (#1327)
Bumps [rack](https://github.com/rack/rack) from 3.1.12 to 3.1.14.
- [Release notes](https://github.com/rack/rack/releases)
- [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rack/rack/compare/v3.1.12...v3.1.14)

---
updated-dependencies:
- dependency-name: rack
  dependency-version: 3.1.14
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-12 17:11:57 -03:00
Rafael dos Santos Silva 67587367ab FEATURE: New setting to control model for translations (#1333) 2025-05-12 12:12:30 -03:00
Kris 1573e6a694 UX: don't show AI suggestions in composer when inputs are disabled (#1331) 2025-05-09 12:34:53 -04:00
Kris 5892e9e66c UX: stick new question button to top of mobile sidebar (#1329) 2025-05-08 17:09:12 -04:00
Isaac Janzen a49ed0cd01 UX: Apply bot conversation sidebar styles to mobile sidebar (#1328) 2025-05-08 15:10:42 -05:00
Sam cf45e6884c FIX: persona triage should be logged to automation (#1326)
We were logging persona triage as "bot" in logs, causing some
confusions around real world usage

This amends it so we log usage to "automation - AUTOMATION NAME"
2025-05-08 12:51:36 +10:00
Sam 2a62658248 FEATURE: support configurable thinking tokens for Gemini (#1322) 2025-05-08 07:39:50 +10:00
Kris 851ca57866 UX: style adjustments for conversations (#1325) 2025-05-07 16:13:25 -04:00
Rafael dos Santos Silva 1b71330cea FIX: Correct prompt format for img2text used in our AI Bot PDF Rag pipeline (#1323) 2025-05-07 16:48:56 -03:00
Isaac Janzen f090065405 DEV: Fix flakey upload spec (#1316) 2025-05-07 10:29:07 -05:00
Discourse Translator Bot c0e15501dd Update translations (#1315) 2025-05-07 15:17:53 +02:00
Sam d62c76d1e8 FIX: do not display empty state while fetching bot conversations (#1320)
* FIX: do not display empty state while fetching bot conversations

* skip test in playwright and fix implementation

* tiny css fix, missing padding
2025-05-07 15:04:35 +10:00
Alan Guo Xiang Tan f1b7cc8f7a DEV: Skip test that is consistently failing on main (#1321) 2025-05-07 09:59:03 +08:00
Sam e3d6dadc41 UX: stop rendering back to forum link (#1319) 2025-05-07 09:07:39 +10:00
Roman Rizzi 5bc9fdc06b FIX: Return structured output on non-streaming mode (#1318) 2025-05-06 15:34:30 -03:00
Keegan George adc2716cec FIX: Invalid access error in logs (#1317) 2025-05-06 10:13:03 -07:00
Roman Rizzi c0a2d4c935 DEV: Use structured responses for summaries (#1252)
* DEV: Use structured responses for summaries

* Fix system specs

* Make response_format a first class citizen and update endpoints to support it

* Response format can be specified in the persona

* lint

* switch to jsonb and make column nullable

* Reify structured output chunks. Move JSON parsing to the depths of Completion

* Switch to JsonStreamingTracker for partial JSON parsing
2025-05-06 10:09:39 -03:00
Sam c6a307b473 FIX: handle unexpected errors when browsing web (#1314) 2025-05-06 18:12:26 +10:00
Sam e9fed4f5fa FEATURE: ensure researcher and github helper know the date (#1312)
The date / time is important cause it creates a bias towards
action.

Without it, model may think it is getting future annoucements vs
up to date info.
2025-05-06 14:39:30 +10:00
Rafael dos Santos Silva 4eac377987 DEV: Zero delays on fake endpoint used in tests (#1311) 2025-05-05 17:47:32 -03:00
Roman Rizzi 48305dc7d3 FIX: resource_url replacemente in Persona's system prompt (#1310) 2025-05-05 11:41:04 -03:00
Sam a6fa619c31 FEATURE: enforce jpg/png for all images (#1309)
This ensures webp / gif are converted to png prior to sending
to LLMs given webp and gif are not evenly supported.

png/jpg is universally supported and are the only supported format.

longer term we need to add support for audio/video/pdf which is supported by some models.

* more specs
2025-05-05 17:46:37 +10:00
Kris b2a6ee9c05 UX: update conversation input styles (#1308) 2025-05-02 13:42:12 -04:00
Sam 3800728d52 FIX: clear uploads after successfully posting new PM (#1307)
This PR addresses a bug where uploads weren't being cleared after successfully posting a new private message in the AI bot conversations interface. Here's what the changes do:

## Main Fix:
- Makes the `prepareAndSubmitToBot()` method async and adds proper error handling
- Adds `this.uploads.clear()` after successful submission to clear all uploads
- Adds a test to verify that the "New Question" button properly resets the UI with no uploads

## Additional Improvements:
1. **Dynamic Character Length Validation**:
   - Uses `siteSettings.min_personal_message_post_length` instead of hardcoded 10 characters
   - Updates the error message to show the dynamic character count
   - Adds proper pluralization in the localization file for the error message

2. **Bug Fixes**:
   - Adds null checks with optional chaining (`link?.topic?.id`) in the sidebar code to prevent potential errors

3. **Code Organization**:
   - Moves error handling from the service to the controller for better separation of concerns
2025-05-02 13:46:22 +10:00
SamandKrzysztof Kotlarek 9196546f6f FIX: better LLM feedback for image generation failures (#1306)
* FIX: handle error conditions when generating images gracefully

* FIX: also handle error for edit_image

* Update lib/inference/open_ai_image_generator.rb

Co-authored-by: Krzysztof Kotlarek <[email protected]>

* lint

---------

Co-authored-by: Krzysztof Kotlarek <[email protected]>
2025-05-01 19:25:38 +10:00
Discourse Translator Bot 47d370588c Update translations (#1294) 2025-05-01 16:11:26 +10:00
Isaac Janzen a55f414667 DEV: Lint discourse-ai-bot-conversations (#1305) 2025-05-01 16:10:47 +10:00
Sam 491dac298f FIX: system persona state leaking between sites (#1304)
System personas leaned on reused classes, this was a problem
in a multisite environement cause state, such as "enabled"
ended up being reused between sites.

New implementation ensures state is pristine between sites in
a multisite

* more handling for new superclass story

* small oversight, display name should be used for display
2025-05-01 13:24:53 +10:00
Samandawesomerobot 8b1b6811f4 FEATURE: add support for uploads when starting a convo (#1301)
This commit introduces file upload capabilities to the AI Bot conversations interface and improves the overall dedicated UX experience. It also changes the experimental setting to a more permanent one.

## Key changes:

- **File upload support**:
  - Integrates UppyUpload for handling file uploads in conversations
  - Adds UI for uploading, displaying, and managing attachments
  - Supports drag & drop, clipboard paste, and manual file selection
  - Shows upload progress indicators for in-progress uploads
  - Appends uploaded file markdown to message content

- **Renamed setting**:
  - Changed `ai_enable_experimental_bot_ux` to `ai_bot_enable_dedicated_ux`
  - Updated setting description to be clearer
  - Changed default value to `true` as this is now a stable feature
  - Added migration to handle the setting name change in database

- **UI improvements**:
  - Enhanced input area with better focus states
  - Improved layout and styling for conversations page
  - Added visual feedback for upload states
  - Better error handling for uploads in progress

- **Code organization**:
  - Refactored message submission logic to handle attachments
  - Updated DOM element IDs for consistency
  - Fixed focus management after submission

- **Added tests**:
  - Tests for file upload functionality
  - Tests for removing uploads before submission
  - Updated existing tests to work with the renamed setting


---------

Co-authored-by: awesomerobot <[email protected]>
2025-05-01 12:21:07 +10:00
Isaac Janzen fc3be6f0ce DEV: Add ai-bot-header-icon plugin outlet (#1302) 2025-04-30 14:15:32 -05:00
Keegan George ab67299acb FIX: Invalid access error should be populated to user (#1303)
Invalid access error should be populated to user when trying to search for something they do not have permissions for (i.e. anons searching `in:messages`
2025-04-30 12:10:10 -07:00
Mark VanLandingham 1e32416eaa UX: Empty state for AI conversations sidebar & btn changes (#1297)
This commit adds an empty state when the user doesn't have any PM history. It ALSO retains the new conversation button in the sidebar so it no longer jumps. The button is disabled, icon, and text are all updated.
2025-04-30 11:33:41 -05:00
Kris 81a664b3da UX: put full page search discoveries in sidebar (#1289) 2025-04-30 12:01:21 -04:00
Sam 7dc3c30fa4 FEATURE: correctly decorate AI bots (#1300)
AI bots come in 2 flavors

1. An LLM and LLM user, in this case we should decorate posts with persona name
2. A Persona user, in this case, in PMs we decorate with LLM name

(2) is a significant improvement, cause previously when creating a conversation
you could not tell which LLM you were talking to by simply looking at the post, you would
have to scroll to the top of the page.

* lint

* translation missing
2025-04-30 16:36:38 +10:00
Sam 8b90ce7c86 FIX: llm selector memory broken (#1299)
llm selector for bot was no longer remembering selected item.
2025-04-30 14:15:28 +10:00
Sérgio Saquetim ede65c971f DEV: Add compatibility with the Glimmer Post Stream (#1230) 2025-04-29 23:55:54 -03:00
Sam 09a6841480 FIX: s3 was missing a const (#1298) 2025-04-30 07:58:24 +10:00
Mark VanLandingham 06ab446e47 UX: Adjust bot conversation header and sidebar on hamburger mode (#1295)
Context in dev conversation -- in hamburger mode we don't need the robot icon to swap to shuffle, AND we don't need back to forum link.
2025-04-29 10:10:44 -05:00
Kris 541e6d7437 UX: increase composer action z-index above ai suggestion (#1296) 2025-04-29 10:42:51 -04:00
SamandAlan Guo Xiang Tan 17f04c76d8 FEATURE: add OpenAI image generation and editing capabilities (#1293)
This commit enhances the AI image generation functionality by adding support for:

1. OpenAI's GPT-based image generation model (gpt-image-1)
2. Image editing capabilities through the OpenAI API
3. A new "Designer" persona specialized in image generation and editing
4. Two new AI tools: CreateImage and EditImage

Technical changes include:
- Renaming `ai_openai_dall_e_3_url` to `ai_openai_image_generation_url` with a migration
- Adding `ai_openai_image_edit_url` setting for the image edit API endpoint
- Refactoring image generation code to handle both DALL-E and the newer GPT models
- Supporting multipart/form-data for image editing requests

* wild guess but maybe quantization is breaking the test sometimes

this increases distance

* Update lib/personas/designer.rb

Co-authored-by: Alan Guo Xiang Tan <[email protected]>

* simplify and de-flake code

* fix, in chat we need enough context so we know exactly what uploads a user uploaded.

* Update lib/personas/tools/edit_image.rb

Co-authored-by: Alan Guo Xiang Tan <[email protected]>

* cleanup downloaded files right away

* fix implementation

---------

Co-authored-by: Alan Guo Xiang Tan <[email protected]>
2025-04-29 17:38:54 +10:00
Renato Atilio 8669e8ae59 Revert "UX: keep composer actions above AI input icons (#1291)" (#1292)
This reverts commit 51a0516aa8.
2025-04-29 08:57:20 +10:00
Kris 51a0516aa8 UX: keep composer actions above AI input icons (#1291)
* UX: keep composer actions above AI input icons

* update comment
2025-04-28 10:45:28 -04:00
Discourse Translator Bot de0a52a0d9 Update translations (#1265) 2025-04-28 13:41:53 +02:00
Isaac Janzen b6630b670f DEV: Update sidebar grouping date labels (#1290)
- Add `Today` label
- Make some minor UX changes
- Add specs
2025-04-26 15:31:22 -05:00
Isaac Janzen 81ef532268 UX: Only scroll when we surpass max input height (#1288) 2025-04-25 13:20:34 -05:00
Isaac Janzen cd0cfc0bfc DEV: Group PMs by date (#1287)
# Preview

https://github.com/user-attachments/assets/3fe3ac8f-c938-4df4-9afe-11980046944d

# Details

- Group pms by `last_posted_at`. In this first iteration we are group by `7 days`, `30 days`, then by month beyond that. 
- I inject a sidebar section link with the relative (last_posted_at) date and then update a tracked value to ensure we don't do it again. Then for each month beyond the first 30days, I add a value to the `loadedMonthLabels` set and we reference that (plus the year) to see if we need to load a new month label.
- I took the creative liberty to remove the `Conversations` section label - this had no purpose
- I hid the _collapse all sidebar sections_ carrot. This had no purpose. 
- Swap `BasicTopicSerializer` to `ListableTopicSerializer` to get access to `last_posted_at`
2025-04-25 13:20:18 -05:00
Joffrey JAFFEUX 60ea590ba5 DEV: invalid selector (#1286)
Playwright is going to be less permissive to this in the future.
2025-04-25 16:56:09 +02:00
Kris 10748015fd UX: minor alignment adjustments for experiemental bot UI (#1285)
* UX: minor alignment adjustments for experiemental bot UI

* account for powered by discourse badge

* breakpoint lint
2025-04-24 17:27:31 -04:00
Mark VanLandingham 797d727a20 UX: Bring back New Conversation button on mobile sidebar (#1283) 2025-04-24 13:47:11 -05:00
Isaac Janzen 1a0d1d6e84 DEV: Add ai-bot-conversations-above-input plugin outlet and update loading animation (#1284)
Update loading animation 

https://github.com/user-attachments/assets/f3bb6ca7-5d84-4231-8ea1-fa49fa402eae


- Add plugin outlet 
- update `updateInputValue` so that it can be used by plugins to update the input value
2025-04-24 13:33:33 -05:00
Mark VanLandingham 298ebee7dd DEV: Migration to backfill bot PM custom field (#1282)
In the last commit, I introduced a topic_custom_field to determine if a PM is indeed a bot PM.

This commit adds a migration to backfill any PM that is between 1 real user, and 1 bot. The correct topic_custom_field is added for these, so they will appear on the bot conversation sidebar properly.

We can also drop the joining to topic_users in the controller for sidebar conversations, and the isPostFromAiBot logic from the sidebar.
2025-04-24 13:02:43 -05:00
Mark VanLandingham b7b9179bc8 FEATURE: Allow for persona & llm selection in bot conversations page (#1276) 2025-04-24 11:17:24 -05:00
Rafael dos Santos Silva 18dda31412 DEV: Update gems for Ruby 3.4 compatibility (#1281) 2025-04-24 13:02:51 -03:00
Sam 65718f6dbe FIX: eat all leading spaces llms provide when they stream them (#1280)
* FIX: eat all leading spaces llms provide when they stream them

* improve so we don't stop replying...
2025-04-24 22:07:26 +10:00
Sam 2060426709 FIX: guard against situations where there is no reply, pass thread id (#1279) 2025-04-24 20:31:14 +10:00
Sam 2a5c60db10 FEATURE: display more places where AI is used / Chat streamer (#1278)
* FEATURE: display more places where AI is used

- Usage was not showing automation or image caption in llm list.
- Also: FIX - reasoning models would time out incorrectly after 60 seconds (raised to 10 minutes)

* correct enum not to enumerate non configured models

* FEATURE: implement chat streamer

This implements a basic chat streamer, it provides 2 things:

1. Gives feedback to the user when LLM is generating
2. Streams stuff much more efficiently to client (given it may take 100ms or so per call to update chat)
2025-04-24 16:22:19 +10:00
Jordan Vidrine e1731dc3df UX: Align styling with conventions (#1275) 2025-04-23 14:14:55 -05:00
Rafael dos Santos Silva 4470e8af9b FIX: Tables should group only per their key on usage page (#1277) 2025-04-23 15:47:34 -03:00
Mark VanLandingham 86f82ea4d3 FIX: Do not re-render conversations sidebar on navigation (#1274) 2025-04-23 15:37:15 +10:00
Joffrey JAFFEUX d2002f81a7 DEV: migrates tools form to form-kit (#1204)
This PR is a retry of: #1135, where we migrate AiTools form to FormKit. The previous PR accidentally removed code related to setting enum values, and as a result was reverted. This update includes enums correctly along with the previous updates.
2025-04-22 09:23:25 -07:00
Isaac Janzen e8b0f86300 FEATURE: Bot Conversation Homepage (#1273) 2025-04-22 10:22:03 -05:00
Mark VanLandingham 244ec9d61e REVERT: "FEATURE: Experimental Private Message Bot Homepage (#1159)" (#1272)
This reverts commit 5fec8fe79e.
2025-04-21 16:42:05 -05:00
Mark VanLandingham 5fec8fe79e FEATURE: Experimental Private Message Bot Homepage (#1159)
Overview
This PR introduces a Bot Homepage that was first introduced at https://ask.discourse.org/.

Key Features:
Add a bot homepage: /discourse-ai/ai-bot/conversations
Display a sidebar with previous bot conversations
Infinite scroll for large counts
Sidebar still visible when navigation mode is header_dropdown
Sidebar visible on homepage and bot PM show view
Add New Question button to the bottom of sidebar on bot PM show view
Add persona picker to homepage
2025-04-21 15:17:10 -05:00
Keegan George d26c7ac48d FEATURE: Add spending metrics to AI usage (#1268)
This update adds metrics for estimated spending in AI usage. To make use of it, admins must add cost details to the LLM config page (input, output, and cached input costs per 1M tokens). After doing so, the metrics will appear in the AI usage dashboard as the AI plugin is used.
2025-04-17 15:09:48 -07:00
Keegan George e2b0287333 FEATURE: Enhance LLM context window settings (#1271)
### 🔍 Overview
This update performs some enhancements to the LLM configuration screen. In particular, it renames the UI for the number of tokens for the prompt to "Context window" since the naming can be confusing to the user. Additionally, it adds a new optional field called "Max output tokens".
2025-04-17 14:44:15 -07:00
Martin Brennan 0c8718e0d5 DEV: Update spec for topic tag suggester (#1269)
This second post was not used and is making the topic scroll
down to post 2, which can mess with editing the OP sometimes
2025-04-17 14:06:45 +10:00
Kris 3440395623 DEV: include excerpt-expanded class when gists are enabled (#1267) 2025-04-16 15:26:04 -04:00
Martin Brennan b5d42f373b DEV: Re-introduce proofreading specs (#1263)
Followup fe7e73a6a4 and
1300cc8a36

Reintroduces proofreader specs and unskips ones skipped when
streaming composer suggestions were added. We just need to make sure
the jobs run immediately in specs because the job is what sends the
MessageBus event to the UI.

Also adds a pageobject method to confirm a diff is shown
in the modal before pressing Confirm button.
2025-04-16 11:11:43 +10:00
Sam 0f34ce999f FIX: omit thinking tokens from chat (#1264)
* FIX: omit thinking tokens from chat

Thinking tokens cause a lot of confusion in chat, get rid of them

* also catch partial tool just in case
2025-04-15 17:13:20 +10:00
Sam 274a54a324 FEATURE: Update model names and specs (#1262)
* FEATURE: Update model names and specs

- not a bug, but made it explicit that tools and thinking are not a chat thing
- updated all models to latest in presets (Gemini and OpenAI)

* allow larger context windows
2025-04-15 16:33:44 +10:00
Martin Brennan fe7e73a6a4 Revert "DEV: Add test to cover proofreading in rich editor (#1259)" (#1261)
This reverts commit e81108abb6.
2025-04-15 15:58:09 +10:00
Martin Brennan e81108abb6 DEV: Add test to cover proofreading in rich editor (#1259)
This core PR must be merged for this to work
https://github.com/discourse/discourse/pull/32280

We want to make sure we don't have regressions with this
functionality in the rich editor, which uses contendeditable
and other APIs than the old composer.
2025-04-15 14:05:09 +10:00
Keegan George 1300cc8a36 FEATURE: Add streaming to composer helper (#1256)
This update adding streaming to the AI helper inside the composer.
2025-04-14 08:18:50 -07:00
Sam 38b492529f FEATURE: improve context management (#1260)
1. Add age of post to topic context (1 month ago, 1 year ago, etc)
2. Refactor code for simplicity
3. Fix handling of post context in DMs which was not using new handling of uploads
2025-04-14 21:45:48 +10:00
Sam 67e3a610cb FIX: invalid context construction for responders (#1257)
Previous to this fix we assumed the name field contained usernames
when in fact it was stored in the id field.

This fixes the context contruction and also adds some basic user
information to the context to assist responders in understanding
the cast of chars
2025-04-12 08:15:31 +10:00
Roman Rizzi df63e36ad8 FEATURE: Make Mixtral tokenizer available for embeddings (#1258) 2025-04-11 12:01:38 -03:00
Keegan George 0024f2d264 FIX: Discovery shouldn't re-trigger when no search (#1255)
This update fixes an issue where discobot discoveries was re-triggering when search menu was opened with no query present.
2025-04-10 10:01:53 -07:00
Keegan George ef08e5ea45 UX: Improve groups list in feature table when many groups (#1254)
This update improves the displaying of groups in the features table when there are many groups for a particular persona.
2025-04-10 09:14:51 -07:00
Keegan George 4de39a07e5 FEATURE: Configure persona backed features in admin panel (#1245)
In this feature update, we add the UI for the ability to easily configure persona backed AI-features. The feature will still be hidden until structured responses are complete.
2025-04-10 08:16:31 -07:00
Keegan George 129ced9088 FIX: Split topic suggester fixes (#1253)
This update fixes a few issues in the split topic suggester. It fixes an issue where not all the category suggestions were appearing in the client. It also fixes an issue where the `move-post` request fails when creating a new topic with only one tag suggestion.
2025-04-09 13:12:34 -07:00
Sam e15984029d FEATURE: allow tools to amend personas (#1250)
Add API methods to AI tools for reading and updating personas, enabling
more flexible AI workflows. This allows custom tools to:

- Fetch persona information through discourse.getPersona()
- Update personas with modified settings via discourse.updatePersona()
- Also update using persona.update()

These APIs enable new use cases like "trainable" moderation bots, where
users with appropriate permissions can set and refine moderation rules
through direct chat interactions, without needing admin panel access.

Also adds a special API scope which allows people to lean on API
for similar actions

Additionally adds a rather powerful hidden feature can allow custom tools
to inject content into the context unconditionally it can be used for memory and similar features
2025-04-09 15:48:25 +10:00
Discourse Translator Bot 0e4bf298b1 Update translations (#1251) 2025-04-08 13:04:39 -04:00
Loïc Guitaut a2ffe9620a DEV: Add system spec to check core features are working fine 2025-04-08 17:04:20 +02:00
Keegan George 97d36524cd FIX: search discovery quirks (#1249)
This update fixes a few small quirks with Discobot discoveries:
- [X] subsequent searches should work correctly
- [X] discovery timeout should maintain UI
- [X] key up/down traversal of regular search results should still work
2025-04-07 12:52:23 -07:00
Roman Rizzi f9d641dd3a FIX: Restore gists previous group access behavior. (#1247)
Previously, allowing "everyone" to access gists meant anons would see them too.
With the move to Personas, we used "[]" to reflect that.

With discourse/discourse#32199 adding the "everyone" option to the personas-allowed
groups, we are switching back to the original behavior.
Leaving allowed groups empty should always mean nobody can use the feature.
2025-04-07 12:04:30 -03:00
Sam ed907dd004 FEATURE: allow to send LLM reports to groups (#1246)
* FEATURE: allow to send LLM reports to groups

* spec regression
2025-04-07 15:31:30 +10:00
Discourse Translator Bot 5ed0990dfc Update translations (#1235) 2025-04-03 00:05:06 +02:00
Sérgio Saquetim aa5c968b18 DEV: Remove the legacy widget post menu code (#1225) 2025-04-02 22:32:31 +01:00
Roman Rizzi fccd072f44 DEV: Don't use delays for streaming summaries. (#1244)
We started used a callback as a buffer in FoldContent, so the Fake endpoint is attempting
to emulate delays in the streaming. However, we don't care about that in these specs.
2025-04-02 13:38:15 -03:00
Roman Rizzi 0d60aca6ef FEATURE: Personas powered summaries. (#1232)
* REFACTOR: Move personas into it's own module.

* WIP: Use personas for summarization

* Prioritize persona default LLM or fallback to newest one

* Simplify summarization strategy

* Keep ai_sumarization_model as a fallback
2025-04-02 12:54:47 -03:00
Kris 32da999144 FIX: less generic animation names (#1243) 2025-04-02 11:28:10 -04:00
Sam 8b573fe743 FIX: maintain newest uploads correctly when constructing context (#1242) 2025-04-02 10:09:38 -03:00
Keegan George a14fe929a9 FIX: return false if no current user personas (#1241) 2025-04-01 13:47:39 -07:00
Kris 7b56d7d4fc UX: adjust artificat UI styles (#1240) 2025-04-01 16:11:36 -04:00
Keegan George f40e094945 FIX: persona setting should compare integer value (#1239)
Oversight from recent feature update: https://github.com/discourse/discourse-ai/pull/1234

We should be comparing integer value of setting, otherwise we will never see continue conversation button.
2025-04-01 11:59:06 -07:00
Keegan George bf5ccb452c FEATURE: Continue conversation from Discobot discovery (#1234)
This feature update allows for continuing the conversation with Discobot Discoveries in an AI bot chat. After discoveries gives you a response to your search you can continue with the existing context.
2025-04-01 10:22:39 -07:00
Kris 5331b6dd8e UX: wider search pane, border, smaller font size (#1238) 2025-04-01 12:57:32 -04:00
David Taylor fb68ae9698 DEV: Drop widget post-menu spec (#1236)
In preparation for https://github.com/discourse/discourse/pull/31211
2025-04-01 15:41:56 +01:00
Roman Rizzi 30242a27e6 REFACTOR: Move personas into its own module. (#1233)
This change moves all the personas code into its own module. We want to treat them as a building block features can built on top of, same as `Completions::Llm`.

The code to title a message was moved from `Bot` to `Playground`.
2025-03-31 14:42:33 -03:00
Sam 5b6d39a206 FEATURE: flexible image handling within messages (#1214)
* DEV: refactor bot internals

This introduces a proper object for bot context, this makes
it simpler to improve context management as we go cause we
have a nice object to work with

Starts refactoring allowing for a single message to have
multiple uploads throughout

* transplant method to message builder

* chipping away at inline uploads

* image support is improved but not fully fixed yet

partially working in anthropic, still got quite a few dialects to go

* open ai and claude are now working

* Gemini is now working as well

* fix nova

* more dialects...

* fix ollama

* fix specs

* update artifact fixed

* more tests

* spam scanner

* pass more specs

* bunch of specs improved

* more bug fixes.

* all the rest of the tests are working

* improve tests coverage and ensure custom tools are aware of new context object

* tests are working, but we need more tests

* resolve merge conflict

* new preamble and expanded specs on ai tool

* remove concept of "standalone tools"

This is no longer needed, we can set custom raw, tool details are injected into tool calls
2025-03-31 12:39:07 -03:00
Keegan Georgeandawesomerobot f3e78f0d80 FIX: Search discoveries improvements (#1228)
**This update makes a few improvements to search discoveries:**
- [x] in search menu panel: search discoveries should still be triggered when no regular results are present
- [x] in full page search: search discoveries should still be triggered when no regular results are present
- [x] flakiness in search discoveries sometimes not working properly.

---------

Co-authored-by: awesomerobot <[email protected]>
2025-03-31 08:38:40 -07:00
Joffrey JAFFEUXandRoman Rizzi 28fa723472 FIX: forced tools wasn't set correctly when tool has no options. (#1231)
* wip: more dynamic availableForcedTools

* FIX: forced tools wasn't set correctly when tool has no options.

---------

Co-authored-by: Roman Rizzi <[email protected]>
2025-03-31 10:45:03 -03:00
Roman Rizzi f43137e067 UX: Show AI bot toggles in the AI bot options section (#1229) 2025-03-27 16:46:35 -03:00
Keegan George da2b083025 FIX: attempt to fix flaky (#1226)
The AI Tool spec that checks that the tool runner will not timeout on slow HTTP request is flaky. In this PR we attempt to resolve the flakiness by:

Ensuring stub_request runs before the request
Increasing the timeout for CI env
2025-03-27 09:34:42 -07:00
Keegan George 6827d63e30 DEV: Streaming animation API for components (#1224)
This takes the logic used in summarization/discoveries for streaming and consolidates it into a single helper lib for smooth streaming. It introduces a new lib: `SmoothStreamer` that can be used by components for smooth streaming text from message bus updates. Additionally, the PR makes use of that new lib in the AI post menu helper.
2025-03-27 08:06:33 -07:00
Keegan George 60fe924ce0 DEV: Remove no longer needed click outside logic (#1223)
The older design approach for sentiment analysis report needed the click outside logic. However, when the design was changed this logic was accidentally left behind. It is potentially causing some negative performance impacts. This PR removes the old unnecessary logic.
2025-03-26 14:24:02 -07:00
Keegan George bab6f0be43 FIX: Ensure category badging present in sentiment reports (#1222)
This PR ensures that the category badges are present in the sentiment analysis report. Since the core change in https://github.com/discourse/discourse/pull/31795, there was a regression in the post list drill-down where category badges were not being shown. This PR fixes that and also ensures icons/emojis are shown when categories make use of them. This PR also adds the category badge in the table list.
2025-03-26 12:37:41 -07:00
Roman Rizzi 1b05da704e FIX: Persona editor keeps dirty data after persisting a single field (#1219) 2025-03-26 15:36:01 -03:00
Keegan George 371146cdfe FIX: Post helper menu results should be selectable (#1221)
This update fixes an issue where the results of the post helper menu was not selectable. Previously, selecting any text inside the menu was immediately closing the menu.
2025-03-26 11:28:57 -07:00
Roman Rizzi a6b08270c0 DEV: GH CI needs discourse-solved (#1220) 2025-03-26 10:12:55 -03:00
Joffrey JAFFEUX 9a752eff29 FIX: do not error when tools are empty (#1218) 2025-03-26 10:42:43 +01:00
Joffrey JAFFEUX 913314b67d FIX: prevents to call Object.keys on null (#1217)
This was causing errors in the forms under specific cases.
2025-03-25 22:35:41 +01:00
Keegan George 9dfae3d472 UX: Convert sentiment analysis overview to horizontal bars (#1216)
The sentiment analysis report page initially showcases sentiments via category/tag by doughnut visualizations. However, this isn't an optimal view for quickly scanning and comparing each result. This PR updates the overview to include a table visualization with horizontal bars to represent sentiment analysis instead of doughnuts. Doughnut visualizations are still maintained however when accessing the sentiment data in the drill down for individual entries.

This approach is an intermediary step, as we will eventually add whole clustering and sizing visualization instead of a table. As such, no relevant tests are added in this PR.
2025-03-25 13:36:52 -07:00
Discourse Translator Bot 76a48786d9 Update translations (#1215) 2025-03-25 17:23:09 +01:00
Natalie Tay f7536181af DEV: Update solved report due to changes in solved plugin (#1212) 2025-03-25 15:15:30 +08:00
SamandNatalie Tay 1dde82eb58 FEATURE: allow specifying tool use none in completion prompt
This PR adds support for disabling further tool calls by setting tool_choice to :none across all supported LLM providers:

- OpenAI: Uses "none" tool_choice parameter
- Anthropic: Uses {type: "none"} and adds a prefill message to prevent confusion
- Gemini: Sets function_calling_config mode to "NONE"
- AWS Bedrock: Doesn't natively support tool disabling, so adds a prefill message

We previously used to disable tool calls by simply removing tool definitions, but this would cause errors with some providers. This implementation uses the supported method appropriate for each provider while providing a fallback for Bedrock.

Co-authored-by: Natalie Tay <[email protected]>

* remove stray puts

* cleaner chain breaker for last tool call (works in thinking)

remove unused code

* improve test

---------

Co-authored-by: Natalie Tay <[email protected]>
2025-03-25 08:06:43 +11:00
Keegan George 50e1bc774a FIX: unable to click doughnut when no filters (#1213)
This PR fixes an issue where you are unable to click to see the sentiment drill-down when there are no current filters applied. This is due to trying to `JSON.parse()` the filters when there are no filters. This fix ensures there are filters first before trying to parse the JSON.
2025-03-24 10:10:22 -07:00
Régis Hanol 2dd7068b46 DEV: add guardian argument to TopicsFilter callback (#1206)
To match the updates in discourse/discourse#31908
2025-03-24 10:33:18 +01:00
Joffrey JAFFEUX 7e4f93724d FIX: bump system prompt limit (#1210)
300 was too low, bumped it to 100_000
2025-03-24 08:47:35 +11:00
Isaac Janzen 95160c80e3 FIX: Remove incorrect full-page-search app event (#1209)
This appEvent is not firing on click, but instead on `resetAiResults`. This action is fired many times during page load.
2025-03-21 14:13:22 -05:00
Roman RizziandJoffrey JAFFEUX 2a8be6e2d7 REFACTOR: Migrate Personas' form to FormKit (#1178)
* REFACTOR: Migrate Personas' form to FormKit

We re-arranged fields into sections so we can better differentiate which options are specific to the AI bot.

* few form-kit improvements

https://github.com/discourse/discourse/pull/31934

---------

Co-authored-by: Joffrey JAFFEUX <[email protected]>
2025-03-21 14:46:33 -03:00
Rafael dos Santos Silva 0e2dd7378f DEV: Support for extra model params in LLM completions (#1208)
This will be useful to start experiment with non-standard features like
structured outputs.
2025-03-21 13:49:23 -03:00
Alan Guo Xiang Tan 01eced74a3 FIX: Ensure that we shutdown thread pool (#1207) 2025-03-21 11:08:36 +08:00
Joffrey JAFFEUX 451f7662b2 FIX: prevents fields in Object to re-render (#1205) 2025-03-20 08:32:37 +01:00
Joffrey JAFFEUX 75a1ade441 DEV: supports for form-kit changes (#1203)
form.Object and form.Collection have been changed. Most importantly formObject doesnt yield name but data. It's not your responsibility to build an array of fields.
2025-03-19 15:01:14 +01:00
Discourse Translator Bot de8624416a Update translations (#1202) 2025-03-18 15:08:26 +01:00
Sam b6483e416d Revert "DEV: Convert tool editor to form kit (#1135)" (#1201)
This reverts commit 107f14456b.

enum was not handled, so reverting for now
2025-03-18 18:07:04 +11:00
407 changed files with 14298 additions and 4852 deletions
+2
View File
@@ -1,3 +1,5 @@
< 3.5.0.beta3-dev: 09a68414804a1447f52e5d60691ba59742cda9ec
< 3.5.0.beta2-dev: de8624416a15b3d8e7ad350b083cc1420451ccec
< 3.5.0.beta1-dev: bdef136080074a993e7c4f5ca562edc31a8ba756
< 3.4.0.beta4-dev: a53719ab8eb071459f215227421b3ea4987e5f87
< 3.4.0.beta4-dev: 20612fde52d3f740cad64823ef8aadb0748b567f
+3
View File
@@ -5,3 +5,6 @@ node_modules
evals/log
evals/cases
config/eval-llms.local.yml
# this gets rid of search results from ag, ripgrep, etc
tokenizers/
public/ai-share/highlight.min.js
+1 -1
View File
@@ -34,7 +34,7 @@ GEM
racc
prettier_print (1.2.1)
racc (1.8.1)
rack (3.1.12)
rack (3.1.14)
rainbow (3.1.1)
regexp_parser (2.10.0)
rubocop (1.74.0)
+2 -1
View File
@@ -1,7 +1,8 @@
{
"tests": {
"requiredPlugins": [
"https://github.com/discourse/discourse-prometheus"
"https://github.com/discourse/discourse-prometheus",
"https://github.com/discourse/discourse-solved.git"
]
}
}
@@ -0,0 +1,27 @@
import { ajax } from "discourse/lib/ajax";
import DiscourseRoute from "discourse/routes/discourse";
import SiteSetting from "admin/models/site-setting";
export default class AdminPluginsShowDiscourseAiFeaturesEdit extends DiscourseRoute {
async model(params) {
const allFeatures = this.modelFor(
"adminPlugins.show.discourse-ai-features"
);
const id = parseInt(params.id, 10);
const currentFeature = allFeatures.find((feature) => feature.id === id);
const { site_settings } = await ajax("/admin/config/site_settings.json", {
data: {
filter_area: `ai-features/${currentFeature.ref}`,
plugin: "discourse-ai",
category: "discourse_ai",
},
});
currentFeature.feature_settings = site_settings.map((setting) =>
SiteSetting.create(setting)
);
return currentFeature;
}
}
@@ -0,0 +1,10 @@
import { service } from "@ember/service";
import DiscourseRoute from "discourse/routes/discourse";
export default class AdminPluginsShowDiscourseAiFeatures extends DiscourseRoute {
@service store;
async model() {
return this.store.findAll("ai-feature");
}
}
@@ -2,8 +2,17 @@ import DiscourseRoute from "discourse/routes/discourse";
export default class AdminPluginsShowDiscourseAiLlmsEdit extends DiscourseRoute {
async model(params) {
const allLlms = this.modelFor("adminPlugins.show.discourse-ai-llms");
const id = parseInt(params.id, 10);
if (id < 0) {
// You shouldn't be able to access the edit page
// if the model is seeded
return this.router.transitionTo(
"adminPlugins.show.discourse-ai-llms.index"
);
}
const allLlms = this.modelFor("adminPlugins.show.discourse-ai-llms");
const record = allLlms.findBy("id", id);
record.provider_params = record.provider_params || {};
return record;
@@ -0,0 +1,24 @@
import RouteTemplate from "ember-route-template";
import BackButton from "discourse/components/back-button";
import SiteSettingComponent from "admin/components/site-setting";
export default RouteTemplate(
<template>
<BackButton
@route="adminPlugins.show.discourse-ai-features"
@label="discourse_ai.features.back"
/>
<section class="ai-feature-editor__header">
<h2>{{@model.name}}</h2>
<p>{{@model.description}}</p>
</section>
<section class="ai-feature-editor">
{{#each @model.feature_settings as |setting|}}
<div>
<SiteSettingComponent @setting={{setting}} />
</div>
{{/each}}
</section>
</template>
);
@@ -0,0 +1,156 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import RouteTemplate from "ember-route-template";
import { gt } from "truth-helpers";
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
import DButton from "discourse/components/d-button";
import DPageSubheader from "discourse/components/d-page-subheader";
import { i18n } from "discourse-i18n";
export default RouteTemplate(
class extends Component {
@service adminPluginNavManager;
get tableHeaders() {
const prefix = "discourse_ai.features.list.header";
return [
i18n(`${prefix}.name`),
i18n(`${prefix}.persona`),
i18n(`${prefix}.groups`),
"",
];
}
get configuredFeatures() {
return this.args.model.filter(
(feature) => feature.enable_setting.value === true
);
}
get unconfiguredFeatures() {
return this.args.model.filter(
(feature) => feature.enable_setting.value === false
);
}
<template>
<DBreadcrumbsItem
@path="/admin/plugins/{{this.adminPluginNavManager.currentPlugin.name}}/ai-features"
@label={{i18n "discourse_ai.features.short_title"}}
/>
<section class="ai-feature-list admin-detail">
<DPageSubheader
@titleLabel={{i18n "discourse_ai.features.short_title"}}
@descriptionLabel={{i18n "discourse_ai.features.description"}}
@learnMoreUrl="todo"
/>
{{#if (gt this.configuredFeatures.length 0)}}
<div class="ai-feature-list__configured-features">
<h3>{{i18n "discourse_ai.features.list.configured_features"}}</h3>
<table class="d-admin-table">
<thead>
<tr>
{{#each this.tableHeaders as |header|}}
<th>{{header}}</th>
{{/each}}
</tr>
</thead>
<tbody>
{{#each this.configuredFeatures as |feature|}}
<tr
class="ai-feature-list__row d-admin-row__content"
data-feature-name={{feature.name}}
>
<td class="d-admin-row__overview ai-feature-list__row-item">
<span class="ai-feature-list__row-item-name">
<strong>
{{feature.name}}
</strong>
</span>
<span class="ai-feature-list__row-item-description">
{{feature.description}}
</span>
</td>
<td
class="d-admin-row__detail ai-feature-list__row-item ai-feature-list__persona"
>
<DButton
class="btn-flat btn-small ai-feature-list__row-item-persona"
@translatedLabel={{feature.persona.name}}
@route="adminPlugins.show.discourse-ai-personas.edit"
@routeModels={{feature.persona.id}}
/>
</td>
<td
class="d-admin-row__detail ai-feature-list__row-item ai-feature-list__groups"
>
{{#if (gt feature.persona.allowed_groups.length 0)}}
<ul class="ai-feature-list__row-item-groups">
{{#each feature.persona.allowed_groups as |group|}}
<li>{{group.name}}</li>
{{/each}}
</ul>
{{/if}}
</td>
<td class="d-admin-row_controls">
<DButton
class="btn-small edit"
@label="discourse_ai.features.list.edit"
@route="adminPlugins.show.discourse-ai-features.edit"
@routeModels={{feature.id}}
/>
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
{{/if}}
{{#if (gt this.unconfiguredFeatures.length 0)}}
<div class="ai-feature-list__unconfigured-features">
<h3>{{i18n "discourse_ai.features.list.unconfigured_features"}}</h3>
<table class="d-admin-table">
<thead>
<tr>
<th>{{i18n "discourse_ai.features.list.header.name"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{#each this.unconfiguredFeatures as |feature|}}
<tr class="ai-feature-list__row d-admin-row__content">
<td class="d-admin-row__overview ai-feature-list__row-item">
<span class="ai-feature-list__row-item-name">
<strong>
{{feature.name}}
</strong>
</span>
<span class="ai-feature-list__row-item-description">
{{feature.description}}
</span>
</td>
<td class="d-admin-row_controls">
<DButton
class="btn-small"
@label="discourse_ai.features.list.set_up"
@route="adminPlugins.show.discourse-ai-features.edit"
@routeModels={{feature.id}}
/>
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
{{/if}}
</section>
</template>
}
);
@@ -0,0 +1,36 @@
# frozen_string_literal: true
module DiscourseAi
module Admin
class AiFeaturesController < ::Admin::AdminController
requires_plugin ::DiscourseAi::PLUGIN_NAME
def index
render json: serialize_features(DiscourseAi::Features.features)
end
def edit
raise Discourse::InvalidParameters.new(:id) if params[:id].blank?
render json: serialize_feature(DiscourseAi::Features.find_feature_by_id(params[:id].to_i))
end
private
def serialize_features(features)
features.map { |feature| feature.merge(persona: serialize_persona(feature[:persona])) }
end
def serialize_feature(feature)
return nil if feature.blank?
feature.merge(persona: serialize_persona(feature[:persona]))
end
def serialize_persona(persona)
return nil if persona.blank?
serialize_data(persona, AiFeaturesPersonaSerializer, root: false)
end
end
end
end
@@ -157,9 +157,13 @@ module DiscourseAi
:provider,
:tokenizer,
:max_prompt_tokens,
:max_output_tokens,
:api_key,
:enabled_chat_bot,
:vision_enabled,
:input_cost,
:cached_input_cost,
:output_cost,
)
provider = updating ? updating.provider : permitted[:provider]
@@ -15,7 +15,7 @@ module DiscourseAi
LocalizedAiPersonaSerializer.new(persona, root: false)
end
tools =
DiscourseAi::AiBot::Personas::Persona.all_available_tools.map do |tool|
DiscourseAi::Personas::Persona.all_available_tools.map do |tool|
AiToolSerializer.new(tool, root: false)
end
AiTool
@@ -221,6 +221,14 @@ module DiscourseAi
permitted[:tools] = permit_tools(tools)
end
if response_format = params.dig(:ai_persona, :response_format)
permitted[:response_format] = permit_response_format(response_format)
end
if examples = params.dig(:ai_persona, :examples)
permitted[:examples] = permit_examples(examples)
end
permitted
end
@@ -235,6 +243,24 @@ module DiscourseAi
[tool, options, !!force_tool]
end
end
def permit_response_format(response_format)
return [] if !response_format.is_a?(Array)
response_format.map do |element|
if element && element.is_a?(ActionController::Parameters)
element.permit!
else
false
end
end
end
def permit_examples(examples)
return [] if !examples.is_a?(Array)
examples.map { |example_arr| example_arr.take(2).map(&:to_s) }
end
end
end
end
@@ -55,7 +55,7 @@ module DiscourseAi
# we need an llm so we have a tokenizer
# but will do without if none is available
llm = LlmModel.first&.to_llm
runner = @ai_tool.runner(parameters, llm: llm, bot_user: current_user, context: {})
runner = @ai_tool.runner(parameters, llm: llm, bot_user: current_user)
result = runner.invoke
if result.is_a?(Hash) && result[:error]
@@ -47,9 +47,9 @@ module DiscourseAi
def discover
ai_persona =
AiPersona.all_personas.find do |persona|
persona.id == SiteSetting.ai_bot_discover_persona.to_i
end
AiPersona
.all_personas(enabled_only: false)
.find { |persona| persona.id == SiteSetting.ai_bot_discover_persona.to_i }
if ai_persona.nil? || !current_user.in_any_groups?(ai_persona.allowed_group_ids.to_a)
raise Discourse::InvalidAccess.new
@@ -69,6 +69,40 @@ module DiscourseAi
render json: {}, status: 200
end
def discover_continue_convo
raise Discourse::InvalidParameters.new("user_id") if !params[:user_id]
raise Discourse::InvalidParameters.new("query") if !params[:query]
raise Discourse::InvalidParameters.new("context") if !params[:context]
user = User.find(params[:user_id])
bot_user_id = AiPersona.find_by(id: SiteSetting.ai_bot_discover_persona).user_id
bot_username = User.find_by(id: bot_user_id).username
query = params[:query]
context = "[quote]\n#{params[:context]}\n[/quote]"
post =
PostCreator.create!(
user,
title:
I18n.t("discourse_ai.ai_bot.discoveries.continue_conversation.title", query: query),
raw:
I18n.t(
"discourse_ai.ai_bot.discoveries.continue_conversation.raw",
query: query,
context: context,
),
archetype: Archetype.private_message,
target_usernames: bot_username,
skip_validations: true,
)
render json: success_json.merge(topic_id: post.topic_id)
rescue StandardError => e
render json: failed_json.merge(errors: [e.message]), status: 422
end
end
end
end
@@ -0,0 +1,39 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
class ConversationsController < ::ApplicationController
requires_plugin ::DiscourseAi::PLUGIN_NAME
requires_login
def index
page = params[:page].to_i
per_page = params[:per_page]&.to_i || 40
base_query =
Topic
.private_messages_for_user(current_user)
.where(user: current_user) # Only show PMs where the current user is the author
.joins(
"INNER JOIN topic_custom_fields tcf ON tcf.topic_id = topics.id
AND tcf.name = '#{DiscourseAi::AiBot::TOPIC_AI_BOT_PM_FIELD}'
AND tcf.value = 't'",
)
.distinct
total = base_query.count
pms = base_query.order(last_posted_at: :desc).offset(page * per_page).limit(per_page)
render json: {
conversations: serialize_data(pms, ListableTopicSerializer),
meta: {
total: total,
page: page,
per_page: per_page,
has_more: total > (page + 1) * per_page,
},
}
end
end
end
end
@@ -43,7 +43,7 @@ module DiscourseAi
prompt,
input,
current_user,
force_default_locale,
force_default_locale: force_default_locale,
),
status: 200
end
@@ -110,26 +110,44 @@ module DiscourseAi
end
def stream_suggestion
post_id = get_post_param!
text = get_text_param!
post = Post.includes(:topic).find_by(id: post_id)
location = params[:location]
raise Discourse::InvalidParameters.new(:location) if !location
prompt = CompletionPrompt.find_by(id: params[:mode])
raise Discourse::InvalidParameters.new(:mode) if !prompt || !prompt.enabled?
raise Discourse::InvalidParameters.new(:post_id) unless post
return suggest_thumbnails(input) if prompt.id == CompletionPrompt::ILLUSTRATE_POST
if prompt.id == CompletionPrompt::CUSTOM_PROMPT
raise Discourse::InvalidParameters.new(:custom_prompt) if params[:custom_prompt].blank?
end
Jobs.enqueue(
:stream_post_helper,
post_id: post.id,
user_id: current_user.id,
text: text,
prompt: prompt.name,
custom_prompt: params[:custom_prompt],
)
if location == "composer"
Jobs.enqueue(
:stream_composer_helper,
user_id: current_user.id,
text: text,
prompt: prompt.name,
custom_prompt: params[:custom_prompt],
force_default_locale: params[:force_default_locale] || false,
)
else
post_id = get_post_param!
post = Post.includes(:topic).find_by(id: post_id)
raise Discourse::InvalidParameters.new(:post_id) unless post
Jobs.enqueue(
:stream_post_helper,
post_id: post.id,
user_id: current_user.id,
text: text,
prompt: prompt.name,
custom_prompt: params[:custom_prompt],
)
end
render json: { success: true }, status: 200
rescue DiscourseAi::Completions::Endpoints::Base::CompletionFailed
@@ -46,11 +46,19 @@ module DiscourseAi
end
hijack do
semantic_search
.search_for_topics(query, _page = 1, hyde: !skip_hyde)
.each { |topic_post| grouped_results.add(topic_post) }
begin
semantic_search
.search_for_topics(query, _page = 1, hyde: !skip_hyde)
.each { |topic_post| grouped_results.add(topic_post) }
render_serialized(grouped_results, GroupedSearchResultSerializer, result: grouped_results)
render_serialized(
grouped_results,
GroupedSearchResultSerializer,
result: grouped_results,
)
rescue Discourse::InvalidAccess
render_json_error(I18n.t("invalid_access"), status: 403)
end
end
end
@@ -46,10 +46,6 @@ module DiscourseAi
u.name,
u.uploaded_avatar_id,
c.id AS category_id,
c.name AS category_name,
c.color AS category_color,
c.slug AS category_slug,
c.description AS category_description,
(CASE
WHEN (cr.classification::jsonb->'positive')::float > :threshold THEN 'positive'
WHEN (cr.classification::jsonb->'negative')::float > :threshold THEN 'negative'
+2 -2
View File
@@ -12,11 +12,11 @@ module ::Jobs
return if message.blank?
personaClass =
DiscourseAi::AiBot::Personas::Persona.find_by(id: args[:persona_id], user: message.user)
DiscourseAi::Personas::Persona.find_by(id: args[:persona_id], user: message.user)
return if personaClass.blank?
user = User.find_by(id: personaClass.user_id)
bot = DiscourseAi::AiBot::Bot.as(user, persona: personaClass.new)
bot = DiscourseAi::Personas::Bot.as(user, persona: personaClass.new)
DiscourseAi::AiBot::Playground.new(bot).reply_to_chat_message(
message,
+4 -4
View File
@@ -10,13 +10,13 @@ module ::Jobs
persona_id = args[:persona_id]
begin
persona = DiscourseAi::AiBot::Personas::Persona.find_by(user: post.user, id: persona_id)
raise DiscourseAi::AiBot::Bot::BOT_NOT_FOUND if persona.nil?
persona = DiscourseAi::Personas::Persona.find_by(user: post.user, id: persona_id)
raise DiscourseAi::Personas::Bot::BOT_NOT_FOUND if persona.nil?
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona.new)
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona.new)
DiscourseAi::AiBot::Playground.new(bot).reply_to(post)
rescue DiscourseAi::AiBot::Bot::BOT_NOT_FOUND
rescue DiscourseAi::Personas::Bot::BOT_NOT_FOUND
Rails.logger.warn(
"Bot not found for post #{post.id} - perhaps persona was deleted or bot was disabled",
)
@@ -0,0 +1,27 @@
# frozen_string_literal: true
module Jobs
class StreamComposerHelper < ::Jobs::Base
sidekiq_options retry: false
def execute(args)
return unless args[:prompt]
return unless user = User.find_by(id: args[:user_id])
return unless args[:text]
prompt = CompletionPrompt.enabled_by_name(args[:prompt])
if prompt.id == CompletionPrompt::CUSTOM_PROMPT
prompt.custom_instruction = args[:custom_prompt]
end
DiscourseAi::AiHelper::Assistant.new.stream_prompt(
prompt,
args[:text],
user,
"/discourse-ai/ai-helper/stream_composer_suggestion",
force_default_locale: args[:force_default_locale],
)
end
end
end
+2
View File
@@ -7,6 +7,8 @@ module Jobs
def execute(args)
interaction = args[:interaction]
return unless SiteSetting.ai_discord_search_enabled
if SiteSetting.ai_discord_search_mode == "persona"
DiscourseAi::Discord::Bot::PersonaReplier.new(interaction).handle_interaction!
else
+11 -7
View File
@@ -9,9 +9,9 @@ module Jobs
return if (query = args[:query]).blank?
ai_persona_klass =
AiPersona.all_personas.find do |persona|
persona.id == SiteSetting.ai_bot_discover_persona.to_i
end
AiPersona
.all_personas(enabled_only: false)
.find { |persona| persona.id == SiteSetting.ai_bot_discover_persona.to_i }
if ai_persona_klass.nil? || !user.in_any_groups?(ai_persona_klass.allowed_group_ids.to_a)
return
@@ -19,7 +19,7 @@ module Jobs
return if (llm_model = LlmModel.find_by(id: ai_persona_klass.default_llm_id)).nil?
bot =
DiscourseAi::AiBot::Bot.as(
DiscourseAi::Personas::Bot.as(
Discourse.system_user,
persona: ai_persona_klass.new,
model: llm_model,
@@ -30,9 +30,13 @@ module Jobs
base = { query: query, model_used: llm_model.display_name }
bot.reply(
{ conversation_context: [{ type: :user, content: query }], skip_tool_details: true },
) do |partial|
context =
DiscourseAi::Personas::BotContext.new(
messages: [{ type: :user, content: query }],
skip_tool_details: true,
)
bot.reply(context) do |partial|
streamed_reply << partial
# Throttle updates.
+54 -16
View File
@@ -13,6 +13,7 @@ class AiPersona < ActiveRecord::Base
validate :system_persona_unchangeable, on: :update, if: :system
validate :chat_preconditions
validate :allowed_seeded_model, if: :default_llm_id
validate :well_formated_examples
validates :max_context_posts, numericality: { greater_than: 0 }, allow_nil: true
# leaves some room for growth but sets a maximum to avoid memory issues
# we may want to revisit this in the future
@@ -46,13 +47,18 @@ class AiPersona < ActiveRecord::Base
scope :ordered, -> { order("priority DESC, lower(name) ASC") }
def self.all_personas
def self.all_personas(enabled_only: true)
persona_cache[:value] ||= AiPersona
.ordered
.where(enabled: true)
.all
.limit(MAX_PERSONAS_PER_SITE)
.map(&:class_instance)
if enabled_only
persona_cache[:value].select { |p| p.enabled }
else
persona_cache[:value]
end
end
def self.persona_users(user: nil)
@@ -176,6 +182,7 @@ class AiPersona < ActiveRecord::Base
description
allowed_group_ids
tool_details
enabled
]
instance_attributes = {}
@@ -201,14 +208,14 @@ class AiPersona < ActiveRecord::Base
if inner_name.start_with?("custom-")
custom_tool_id = inner_name.split("-", 2).last.to_i
if AiTool.exists?(id: custom_tool_id, enabled: true)
klass = DiscourseAi::AiBot::Tools::Custom.class_instance(custom_tool_id)
klass = DiscourseAi::Personas::Tools::Custom.class_instance(custom_tool_id)
end
else
inner_name = inner_name.gsub("Tool", "")
inner_name = "List#{inner_name}" if %w[Categories Tags].include?(inner_name)
begin
klass = "DiscourseAi::AiBot::Tools::#{inner_name}".constantize
klass = "DiscourseAi::Personas::Tools::#{inner_name}".constantize
options[klass] = current_options if current_options
rescue StandardError
end
@@ -218,19 +225,26 @@ class AiPersona < ActiveRecord::Base
klass
end
persona_class = DiscourseAi::AiBot::Personas::Persona.system_personas_by_id[self.id]
persona_class = DiscourseAi::Personas::Persona.system_personas_by_id[self.id]
if persona_class
instance_attributes.each do |key, value|
# description/name are localized
persona_class.define_singleton_method(key) { value } if key != :description && key != :name
end
persona_class.define_method(:options) { options }
return persona_class
return(
# we need a new copy so we don't leak information
# across sites
Class.new(persona_class) do
# required for localization
define_singleton_method(:to_s) { persona_class.to_s }
instance_attributes.each do |key, value|
# description/name are localized
define_singleton_method(key) { value } if key != :description && key != :name
end
define_method(:options) { options }
end
)
end
ai_persona_id = self.id
Class.new(DiscourseAi::AiBot::Personas::Persona) do
Class.new(DiscourseAi::Personas::Persona) do
instance_attributes.each { |key, value| define_singleton_method(key) { value } }
define_singleton_method(:to_s) do
@@ -252,6 +266,7 @@ class AiPersona < ActiveRecord::Base
define_method(:top_p) { @ai_persona&.top_p }
define_method(:system_prompt) { @ai_persona&.system_prompt || "You are a helpful bot." }
define_method(:uploads) { @ai_persona&.uploads }
define_method(:examples) { @ai_persona&.examples }
end
end
@@ -312,9 +327,11 @@ class AiPersona < ActiveRecord::Base
end
def system_persona_unchangeable
error_msg = I18n.t("discourse_ai.ai_bot.personas.cannot_edit_system_persona")
if top_p_changed? || temperature_changed? || system_prompt_changed? || name_changed? ||
description_changed?
errors.add(:base, I18n.t("discourse_ai.ai_bot.personas.cannot_edit_system_persona"))
errors.add(:base, error_msg)
elsif tools_changed?
old_tools = tools_change[0]
new_tools = tools_change[1]
@@ -322,9 +339,17 @@ class AiPersona < ActiveRecord::Base
old_tool_names = old_tools.map { |t| t.is_a?(Array) ? t[0] : t }.to_set
new_tool_names = new_tools.map { |t| t.is_a?(Array) ? t[0] : t }.to_set
if old_tool_names != new_tool_names
errors.add(:base, I18n.t("discourse_ai.ai_bot.personas.cannot_edit_system_persona"))
end
errors.add(:base, error_msg) if old_tool_names != new_tool_names
elsif response_format_changed?
old_format = response_format_change[0].map { |f| f["key"] }.to_set
new_format = response_format_change[1].map { |f| f["key"] }.to_set
errors.add(:base, error_msg) if old_format != new_format
elsif examples_changed?
old_examples = examples_change[0].flatten.to_set
new_examples = examples_change[1].flatten.to_set
errors.add(:base, error_msg) if old_examples != new_examples
end
end
@@ -345,6 +370,17 @@ class AiPersona < ActiveRecord::Base
errors.add(:default_llm, I18n.t("discourse_ai.llm.configuration.invalid_seeded_model"))
end
def well_formated_examples
return if examples.blank?
if examples.is_a?(Array) &&
examples.all? { |e| e.is_a?(Array) && e.length == 2 && e.all?(&:present?) }
return
end
errors.add(:examples, I18n.t("discourse_ai.personas.malformed_examples"))
end
end
# == Schema Information
@@ -382,6 +418,8 @@ end
# rag_llm_model_id :bigint
# default_llm_id :bigint
# question_consolidator_llm_id :bigint
# response_format :jsonb
# examples :jsonb
#
# Indexes
#
+162 -82
View File
@@ -35,8 +35,8 @@ class AiTool < ActiveRecord::Base
tool_name.presence || name
end
def runner(parameters, llm:, bot_user:, context: {})
DiscourseAi::AiBot::ToolRunner.new(
def runner(parameters, llm:, bot_user:, context: nil)
DiscourseAi::Personas::ToolRunner.new(
parameters: parameters,
llm: llm,
bot_user: bot_user,
@@ -59,86 +59,166 @@ class AiTool < ActiveRecord::Base
def self.preamble
<<~JS
/**
* Tool API Quick Reference
*
* Entry Functions
*
* invoke(parameters): Main function. Receives parameters (Object). Must return a JSON-serializable value.
* Example:
* function invoke(parameters) { return "result"; }
*
* details(): Optional. Returns a string describing the tool.
* Example:
* function details() { return "Tool description."; }
*
* Provided Objects
*
* 1. http
* http.get(url, options?): Performs an HTTP GET request.
* Parameters:
* url (string): The request URL.
* options (Object, optional):
* headers (Object): Request headers.
* Returns:
* { status: number, body: string }
*
* http.post(url, options?): Performs an HTTP POST request.
* Parameters:
* url (string): The request URL.
* options (Object, optional):
* headers (Object): Request headers.
* body (string): Request body.
* Returns:
* { status: number, body: string }
*
* (also available: http.put, http.patch, http.delete)
*
* Note: Max 20 HTTP requests per execution.
*
* 2. llm
* llm.truncate(text, length): Truncates text to a specified token length.
* Parameters:
* text (string): Text to truncate.
* length (number): Max tokens.
* Returns:
* Truncated string.
*
* 3. index
* index.search(query, options?): Searches indexed documents.
* Parameters:
* query (string): Search query.
* options (Object, optional):
* filenames (Array): Limit search to specific files.
* limit (number): Max fragments (up to 200).
* Returns:
* Array of { fragment: string, metadata: string }
*
* 4. upload
* upload.create(filename, base_64_content): Uploads a file.
* Parameters:
* filename (string): Name of the file.
* base_64_content (string): Base64 encoded file content.
* Returns:
* { id: number, short_url: string }
*
* 5. chain
* chain.setCustomRaw(raw): Sets the body of the post and exist chain.
* Parameters:
* raw (string): raw content to add to post.
*
* Constraints
*
* Execution Time: ≤ 2000ms
* Memory: ≤ 10MB
* HTTP Requests: ≤ 20 per execution
* Exceeding limits will result in errors or termination.
*
* Security
*
* Sandboxed Environment: No access to system or global objects.
* No File System Access: Cannot read or write files.
*/
/**
* Tool API Quick Reference
*
* Entry Functions
*
* invoke(parameters): Main function. Receives parameters defined in the tool's signature (Object).
* Must return a JSON-serializable value (e.g., string, number, object, array).
* Example:
* function invoke(parameters) { return { result: "Data processed", input: parameters.query }; }
*
* details(): Optional function. Returns a string (can include basic HTML) describing
* the tool's action after invocation, often using data from the invocation.
* This is displayed in the chat interface.
* Example:
* let lastUrl;
* function invoke(parameters) {
* lastUrl = parameters.url;
* // ... perform action ...
* return { success: true, content: "..." };
* }
* function details() {
* return `Browsed: <a href="${lastUrl}">${lastUrl}</a>`;
* }
*
* Provided Objects & Functions
*
* 1. http
* Performs HTTP requests. Max 20 requests per execution.
*
* http.get(url, options?): Performs GET request.
* Parameters:
* url (string): The request URL.
* options (Object, optional):
* headers (Object): Request headers (e.g., { "Authorization": "Bearer key" }).
* Returns: { status: number, body: string }
*
* http.post(url, options?): Performs POST request.
* Parameters:
* url (string): The request URL.
* options (Object, optional):
* headers (Object): Request headers.
* body (string | Object): Request body. If an object, it's stringified as JSON.
* Returns: { status: number, body: string }
*
* http.put(url, options?): Performs PUT request (similar to POST).
* http.patch(url, options?): Performs PATCH request (similar to POST).
* http.delete(url, options?): Performs DELETE request (similar to GET/POST).
*
* 2. llm
* Interacts with the Language Model.
*
* llm.truncate(text, length): Truncates text to a specified token length based on the configured LLM's tokenizer.
* Parameters:
* text (string): Text to truncate.
* length (number): Maximum number of tokens.
* Returns: string (truncated text)
*
* llm.generate(prompt): Generates text using the configured LLM associated with the tool runner.
* Parameters:
* prompt (string | Object): The prompt. Can be a simple string or an object
* like { messages: [{ type: "system", content: "..." }, { type: "user", content: "..." }] }.
* Returns: string (generated text)
*
* 3. index
* Searches attached RAG (Retrieval-Augmented Generation) documents linked to this tool.
*
* index.search(query, options?): Searches indexed document fragments.
* Parameters:
* query (string): The search query used for semantic search.
* options (Object, optional):
* filenames (Array<string>): Filter search to fragments from specific uploaded filenames.
* limit (number): Maximum number of fragments to return (default: 10, max: 200).
* Returns: Array<{ fragment: string, metadata: string | null }> - Ordered by relevance.
*
* 4. upload
* Handles file uploads within Discourse.
*
* upload.create(filename, base_64_content): Uploads a file created by the tool, making it available in Discourse.
* Parameters:
* filename (string): The desired name for the file (basename is used for security).
* base_64_content (string): Base64 encoded content of the file.
* Returns: { id: number, url: string, short_url: string } - Details of the created upload record.
*
* 5. chain
* Controls the execution flow.
*
* chain.setCustomRaw(raw): Sets the final raw content of the bot's post and immediately
* stops the tool execution chain. Useful for tools that directly
* generate the full response content (e.g., image generation tools attaching the image markdown).
* Parameters:
* raw (string): The raw Markdown content for the post.
* Returns: void
*
* 6. discourse
* Interacts with Discourse specific features. Access is generally performed as the SystemUser.
*
* discourse.search(params): Performs a Discourse search.
* Parameters:
* params (Object): Search parameters (e.g., { search_query: "keyword", with_private: true, max_results: 10 }).
* `with_private: true` searches across all posts visible to the SystemUser. `result_style: 'detailed'` is used by default.
* Returns: Object (Discourse search results structure, includes posts, topics, users etc.)
*
* discourse.getPost(post_id): Retrieves details for a specific post.
* Parameters:
* post_id (number): The ID of the post.
* Returns: Object (Post details including `raw`, nested `topic` object with ListableTopicSerializer structure) or null if not found/accessible.
*
* discourse.getTopic(topic_id): Retrieves details for a specific topic.
* Parameters:
* topic_id (number): The ID of the topic.
* Returns: Object (Topic details using ListableTopicSerializer structure) or null if not found/accessible.
*
* discourse.getUser(user_id_or_username): Retrieves details for a specific user.
* Parameters:
* user_id_or_username (number | string): The ID or username of the user.
* Returns: Object (User details using UserSerializer structure) or null if not found.
*
* discourse.getPersona(name): Gets an object representing another AI Persona configured on the site.
* Parameters:
* name (string): The name of the target persona.
* Returns: Object { respondTo: function(params) } or null if persona not found.
* respondTo(params): Instructs the target persona to generate a response within the current context (e.g., replying to the same post or chat message).
* Parameters:
* params (Object, optional): { instructions: string, whisper: boolean }
* Returns: { success: boolean, post_id?: number, post_number?: number, message_id?: number } or { error: string }
*
* discourse.createChatMessage(params): Creates a new message in a Discourse Chat channel.
* Parameters:
* params (Object): { channel_name: string, username: string, message: string }
* `channel_name` can be the channel name or slug.
* `username` specifies the user who should appear as the sender. The user must exist.
* The sending user must have permission to post in the channel.
* Returns: { success: boolean, message_id?: number, message?: string, created_at?: string } or { error: string }
*
* 7. context
* An object containing information about the environment where the tool is being run.
* Available properties depend on the invocation context, but may include:
* post_id (number): ID of the post triggering the tool (if in a Post context).
* topic_id (number): ID of the topic (if in a Post context).
* private_message (boolean): Whether the context is a private message (in Post context).
* message_id (number): ID of the chat message triggering the tool (if in Chat context).
* channel_id (number): ID of the chat channel (if in Chat context).
* user (Object): Details of the user invoking the tool/persona (structure may vary, often null or SystemUser details unless explicitly passed).
* participants (string): Comma-separated list of usernames in a PM (if applicable).
* // ... other potential context-specific properties added by the calling environment.
*
* Constraints
*
* Execution Time: ≤ 2000ms (default timeout in milliseconds) - This timer *pauses* during external HTTP requests or LLM calls initiated via `http.*` or `llm.generate`, but applies to the script's own processing time.
* Memory: ≤ 10MB (V8 heap limit)
* Stack Depth: ≤ 20 (Marshal stack depth limit for Ruby interop)
* HTTP Requests: ≤ 20 per execution
* Exceeding limits will result in errors or termination (e.g., timeout error, out-of-memory error, TooManyRequestsError).
*
* Security
*
* Sandboxed Environment: The script runs in a restricted V8 JavaScript environment (via MiniRacer).
* No direct access to browser or environment, browser globals (like `window` or `document`), or the host system's file system.
* Network requests are proxied through the Discourse backend, not made directly from the sandbox.
*/
JS
end
+1
View File
@@ -23,6 +23,7 @@ class EmbeddingDefinition < ActiveRecord::Base
DiscourseAi::Tokenizer::GeminiTokenizer,
DiscourseAi::Tokenizer::MultilingualE5LargeTokenizer,
DiscourseAi::Tokenizer::OpenAiTokenizer,
DiscourseAi::Tokenizer::MixtralTokenizer,
].map(&:name)
end
+14
View File
@@ -13,6 +13,14 @@ class LlmModel < ActiveRecord::Base
validates :url, presence: true, unless: -> { provider == BEDROCK_PROVIDER_NAME }
validates_presence_of :name, :api_key
validates :max_prompt_tokens, numericality: { greater_than: 0 }
validates :input_cost,
:cached_input_cost,
:output_cost,
:max_output_tokens,
numericality: {
greater_than_or_equal_to: 0,
},
allow_nil: true
validate :required_provider_params
scope :in_use,
-> do
@@ -55,6 +63,8 @@ class LlmModel < ActiveRecord::Base
},
google: {
disable_native_tools: :checkbox,
enable_thinking: :checkbox,
thinking_tokens: :number,
},
azure: {
disable_native_tools: :checkbox,
@@ -183,4 +193,8 @@ end
# enabled_chat_bot :boolean default(FALSE), not null
# provider_params :jsonb
# vision_enabled :boolean default(FALSE), not null
# input_cost :float
# cached_input_cost :float
# output_cost :float
# max_output_tokens :integer
#
@@ -0,0 +1,12 @@
# frozen_string_literal: true
class AiFeaturesPersonaSerializer < ApplicationSerializer
attributes :id, :name, :system_prompt, :allowed_groups, :enabled
def allowed_groups
Group
.where(id: object.allowed_group_ids)
.pluck(:id, :name)
.map { |id, name| { id: id, name: name } }
end
end
@@ -11,7 +11,7 @@ class AiSentimentPostSerializer < ApplicationSerializer
:excerpt,
:sentiment,
:truncated,
:category,
:category_id,
:created_at
def avatar_template
@@ -25,14 +25,4 @@ class AiSentimentPostSerializer < ApplicationSerializer
def truncated
object.post_cooked.length > SiteSetting.post_excerpt_maxlength
end
def category
{
id: object.category_id,
name: object.category_name,
color: object.category_color,
slug: object.category_slug,
description: object.category_description,
}
end
end
+10
View File
@@ -22,6 +22,9 @@ class AiUsageSerializer < ApplicationSerializer
total_cached_tokens
total_request_tokens
total_response_tokens
input_spending
output_spending
cached_input_spending
],
)
end
@@ -35,6 +38,9 @@ class AiUsageSerializer < ApplicationSerializer
total_cached_tokens
total_request_tokens
total_response_tokens
input_spending
output_spending
cached_input_spending
],
)
end
@@ -49,6 +55,9 @@ class AiUsageSerializer < ApplicationSerializer
total_cached_tokens: user.total_cached_tokens,
total_request_tokens: user.total_request_tokens,
total_response_tokens: user.total_response_tokens,
input_spending: user.input_spending,
output_spending: user.output_spending,
cached_input_spending: user.cached_input_spending,
}
end
end
@@ -60,6 +69,7 @@ class AiUsageSerializer < ApplicationSerializer
total_request_tokens: object.total_request_tokens,
total_response_tokens: object.total_response_tokens,
total_requests: object.total_requests,
total_spending: object.total_spending,
date_range: {
start: object.start_date,
end: object.end_date,

Some files were not shown because too many files have changed in this diff Show More