Compare commits

..
Author SHA1 Message Date
awesomerobot 522c1aabb8 wip experiment with /filter route 2024-12-20 15:55:49 -05:00
Discourse Translator Bot a4033e2af9 Update translations (#1032) 2024-12-18 15:19:47 +01:00
Martin Brennan f35db8068b DEV: Change to use DPageSubheader (#1033)
Previously was AdminPageSubheader until https://github.com/discourse/discourse/pull/30146
2024-12-18 17:39:31 +10:00
Sam fae2d5ff2c FEATURE: link correctly to filters to assist in debugging spam (#1031)
- Add spam_score_type to AiSpamSerializer for better integration with reviewables.
- Introduce a custom filter for detecting AI spam false negatives in moderation workflows.
- Refactor spam report generation to improve identification of false negatives.
- Add tests to verify the custom filter and its behavior.
- Introduce links for all spam counts in report
2024-12-17 11:02:18 +11:00
Keegan George 90ce942108 FEATURE: Add periodic problem checks for each LLM in use (#1020)
This feature adds a periodic problem check which periodically checks for issues with LLMs that are in use. Periodically, we will run a test to see if the in use LLMs are still operational. If it is not, the LLM with the problem is surfaced to the admin so they can easily go and update the configuration.
2024-12-16 15:00:05 -08:00
Mark VanLandingham 24b107881a FEATURE: Unavailable state for semantic search when sort is not Relevant (#1030)
This commit adds an "unavailable" state for the AI semantic search toggle. Currently the AI toggle disappears when the sort by is anything but Relevance which makes the UI confusing for users looking for AI results. This should help!
2024-12-16 14:30:11 -06:00
Roman Rizzi 534b0df391 REFACTOR: Separation of concerns for embedding generation. (#1027)
In a previous refactor, we moved the responsibility of querying and storing embeddings into the `Schema` class. Now, it's time for embedding generation.

The motivation behind these changes is to isolate vector characteristics in simple objects to later replace them with a DB-backed version, similar to what we did with LLM configs.
2024-12-16 09:55:39 -03:00
Martin Brennan 222e2cf4f9 UX: Use new DStatTiles reusable component from core (#1025)
For the Spam and Usage tabs in admin
2024-12-16 16:48:46 +10:00
Roman RizziandKeegan George 94b85ece80 FIX: Make sure gists are atleast five minutes old before updating them (#1029)
* FIX: Make sure gists are atleast five minutes old before updating them

* Update app/jobs/regular/fast_track_topic_gist.rb

Co-authored-by: Keegan George <[email protected]>

---------

Co-authored-by: Keegan George <[email protected]>
2024-12-13 19:36:34 -03:00
Roman Rizzi 1c40a698ca FIX: get strategy version through vector_rep (#1028) 2024-12-13 18:49:18 -03:00
David Taylor fae1fbc796 DEV: Correct i18n lint violation (#1026) 2024-12-13 16:01:04 +00:00
Roman Rizzi eae527f99d REFACTOR: A Simpler way of interacting with embeddings tables. (#1023)
* REFACTOR: A Simpler way of interacting with embeddings' tables.

This change adds a new abstraction called `Schema`, which acts as a repository that supports the same DB features `VectorRepresentation::Base` has, with the exception that removes the need to have duplicated methods per embeddings table.

It is also a bit more flexible when performing a similarity search because you can pass it a block that gives you access to the builder, allowing you to add multiple joins/where conditions.
2024-12-13 10:15:21 -03:00
Roman Rizzi 97ec2c5ff4 FEATURE: Show gists everywhere except suggested/related (#995) 2024-12-12 12:29:35 -03:00
Krzysztof Kotlarek 04c4ff8cf0 UX: No admin header for edit personas tools or llms (#1021)
In this PR, we added functionality to hide the admin header for edit/new actions - https://github.com/discourse/discourse/pull/30175

To make it work properly, we have to rename `show` to `edit` which is also a more accurate name.
2024-12-12 10:48:58 +11:00
Sam 47c1ea337e FIX: allow scanning of trashed posts and deleted users for test (#1024)
When a post is trashed we should still be allowed to scan it
post.topic will be nil for a trashed topic even if post is trashed
2024-12-12 10:26:05 +11:00
47f5da7e42 FEATURE: Add AI-powered spam detection for new user posts (#1004)
This introduces a comprehensive spam detection system that uses LLM models
to automatically identify and flag potential spam posts. The system is
designed to be both powerful and configurable while preventing false positives.

Key Features:
* Automatically scans first 3 posts from new users (TL0/TL1)
* Creates dedicated AI flagging user to distinguish from system flags
* Tracks false positives/negatives for quality monitoring
* Supports custom instructions to fine-tune detection
* Includes test interface for trying detection on any post

Technical Implementation:
* New database tables:
  - ai_spam_logs: Stores scan history and results
  - ai_moderation_settings: Stores LLM config and custom instructions
* Rate limiting and safeguards:
  - Minimum 10-minute delay between rescans
  - Only scans significant edits (>10 char difference)
  - Maximum 3 scans per post
  - 24-hour maximum age for scannable posts
* Admin UI features:
  - Real-time testing capabilities
  - 7-day statistics dashboard
  - Configurable LLM model selection
  - Custom instruction support

Security and Performance:
* Respects trust levels - only scans TL0/TL1 users
* Skips private messages entirely
* Stops scanning users after 3 successful public posts
* Includes comprehensive test coverage
* Maintains audit log of all scan attempts


---------

Co-authored-by: Keegan George <[email protected]>
Co-authored-by: Martin Brennan <[email protected]>
2024-12-12 09:17:25 +11:00
Martin Brennan ae80494448 UX: Improve rough edges of AI usage page (#1014)
* UX: Improve rough edges of AI usage page

* Ensure all text uses I18n
* Change from <button> usage to <DButton>
* Use <AdminConfigAreaCard> in place of custom card styles
* Format numbers nicely using our number format helper,
  show full values on hover using title attr
* Ensure 0 is always shown for counters, instead of being blank

* FEATURE: Load usage data after page load

Use ConditionalLoadingSpinner to hide load of usage
data, this prevents us hanging on page load with a white
screen.

* UX: Split users table, and add empty placeholders and page subheader

* DEV: Test fix
2024-12-12 08:55:24 +11:00
Keegan George a4440c507b UX: Make sentiment trends more readable (#1018)
Instead of a stacked chart showing a separate series for positive and negative, this PR introduces a simplification to the overall sentiment dashboard. It comprises the sentiment into a single series of the difference between `positive - negative` instead. This should allow for the data to be more easy to scan and look for trends
2024-12-11 09:13:18 -08:00
Roman Rizzi 5fc7a730ef FIX: Triage rule should append selected tags instead of replacing them (#1022) 2024-12-11 11:19:44 -03:00
Discourse Translator Bot 34f43f398d Update translations (#1015) 2024-12-10 20:16:44 +01:00
Roman Rizzi 6da35d8e66 FIX: Gemini inference client was missing #instance (#1019) 2024-12-10 15:42:31 -03:00
Keegan George 700e9de073 Revert "UX: Make sentiment trends more readable in time series data (#1013)" (#1016)
This reverts commit 375dd702b2.
2024-12-10 08:15:27 -08:00
Keegan George 375dd702b2 UX: Make sentiment trends more readable in time series data (#1013)
Instead of a stacked chart showing a separate series for positive and negative, this PR introduces a simplification to the overall sentiment dashboard. It comprises the sentiment into a single series of the difference between `positive - negative` instead. This should allow for the data to be more easy to scan and look for trends.
2024-12-10 07:22:41 -08:00
Sam 7ca21cc329 FEATURE: first class support for OpenRouter (#1011)
* FEATURE: first class support for OpenRouter

This new implementation supports picking quantization and provider pref

Also:

- Improve logging for summary generation
- Improve error message when contacting LLMs fails

* Better support for full screen artifacts on iPad

Support back button to close full screen
2024-12-10 05:59:19 +11:00
Kris 51e0a96e51 UX: ensure composer popup is always above AI icons (#1012) 2024-12-09 13:03:27 -05:00
Discourse Translator Bot 6ce14a778b Update translations (#1009) 2024-12-06 18:05:02 +01:00
Roman Rizzi 085dde7042 FEATURE: Select stop sequences from triage script (#1010) 2024-12-06 11:13:47 -03:00
Roman Rizzi 7ebbcd2de3 FIX: Make sure prompt uploads get included in the prompt when triaging (#1008) 2024-12-05 21:04:35 -03:00
Sam a55216773a FEATURE: Amazon Nova support via bedrock (#997)
Refactor dialect selection and add Nova API support

    Change dialect selection to use llm_model object instead of just provider name
    Add support for Amazon Bedrock's Nova API with native tools
    Implement Nova-specific message processing and formatting
    Update specs for Nova and AWS Bedrock endpoints
    Enhance AWS Bedrock support to handle Nova models
    Fix Gemini beta API detection logic
2024-12-06 07:45:58 +11:00
Kelv 5e87a50202 DEV: Update more deprecated Font Awesome icon names (#1005)
* DEV: Update more deprecated Font Awesome icon names

* update to trash-can
2024-12-06 07:45:05 +11:00
Kris 70fae2b699 FIX: close link in shared conversation model (#1007) 2024-12-05 15:19:36 -05:00
Rafael dos Santos Silva 5ba274f7a5 FIX: Change how we filter date for emotion on /filter (#1006) 2024-12-05 14:10:12 -03:00
Roman Rizzi 4ba74511c2 FIX: Make sure limits are updated and applied on each step (#1002) 2024-12-05 10:31:39 -03:00
Kris 50f61925eb FIX: typo in topic gist for escapedExcerpt (#1003) 2024-12-05 08:03:08 -05:00
Kris 625fd06744 UX: improve layout of inline title editing buttons (#1000) 2024-12-04 17:04:26 -05:00
Roman Rizzi b32b1cf241 FIX: Add a digest check to avoid repeteadly generating embeddings (bulk) (#1001) 2024-12-04 17:47:28 -03:00
Keegan George d6beac48f8 DEV: Improve explain suggestion footnote replacement (#999)
Previously, when clicking add footnote on an explain suggestion it would replace the selected word by finding the first occurrence of the word. This results in issues when there are more than one occurrences of a word in a post. This is not trivial to solve, so this PR instead prevents incorrect text replacements by only allowing the replacement if it's unique. We use the same logic here that we use to determine if something can be fast edited.

In this PR we also update tests for post helper explain suggestions. For a while, we haven't had tests here due to streaming/timing issues, we've been skipping our system specs. In this PR, we add acceptance tests to handle this which gives us improved ability to publish message bus updates in the testing environment so that it can be better tested without issues.
2024-12-04 11:41:34 -08:00
Rafael dos Santos Silva 938d4c018c FIX: More resilient sentiment backfill query (#998) 2024-12-04 12:10:31 -03:00
Discourse Translator Bot e7c2cd861a Update translations (#994) 2024-12-04 11:59:37 +01:00
Kelv f8a81a0beb DEV: Update deprecated Font Awesome icon names (#996) 2024-12-04 03:18:08 +01:00
KrisandKeegan George 8203bdfbc9 UX: move topic summary from DMenu to DModal (#992)
Co-authored-by: Keegan George <[email protected]>
2024-12-03 13:30:15 -05:00
Roman Rizzi ce6a2eca21 FEATURE: Backfill posts sentiment. (#982)
* FEATURE: Backfill posts sentiment.

It adds a scheduled job to backfill posts' sentiment, similar to our existing rake task, but with two settings to control the batch size and posts' max-age.

* Make sure model_name order is consistent.
2024-12-03 10:27:03 -03:00
Sam 7c65dd171f FIX: regression, no longer sending examples to AI helper (#993)
For a while now we have not been sending the examples to AI
helper, which can lead to inconsistent results.

Note: this also means that in non English we did not send
English results, so this may end up reducing performance

That said first thing we need to do is fix the regression.
2024-12-03 16:03:46 +11:00
Rafael dos Santos Silva e3f5e86dc5 FIX: AI Automation scripts were broken when using seeded models (#991) 2024-12-02 19:07:05 -03:00
Tobias Eigen fcf2350652 changed "contact this model to "use this model" server.en.yml (#989)
translators gave feedback that the phrase "contact this model" is confusing. changed it to "use this model".
2024-12-03 07:26:54 +11:00
Keegan George fc88bb08ab FIX: Tag suggester is suggesting already assigned tags (#990)
This PR fixes an issue where the tag suggester for edit title topic area was suggesting tags that are already assigned on a post. It also updates the amount of suggested tags to 7 so that there is still a decent amount of tags suggested when tags are already assigned.
2024-12-03 07:25:04 +11:00
Sam 117c06220e FEATURE: allow artifacts to be updated (#980)
Add support for versioned artifacts with improved diff handling

* Add versioned artifacts support allowing artifacts to be updated and tracked
  - New `ai_artifact_versions` table to store version history
  - Support for updating artifacts through a new `UpdateArtifact` tool
  - Add version-aware artifact rendering in posts
  - Include change descriptions for version tracking

* Enhance artifact rendering and security
  - Add support for module-type scripts and external JS dependencies
  - Expand CSP to allow trusted CDN sources (unpkg, cdnjs, jsdelivr, googleapis)
  - Improve JavaScript handling in artifacts

* Implement robust diff handling system (this is dormant but ready to use once LLMs catch up)
  - Add new DiffUtils module for applying changes to artifacts
  - Support for unified diff format with multiple hunks
  - Intelligent handling of whitespace and line endings
  - Comprehensive error handling for diff operations

* Update routes and UI components
  - Add versioned artifact routes
  - Update markdown processing for versioned artifacts

Also

- Tweaks summary prompt
- Improves upload support in custom tool to also provide urls
2024-12-03 07:23:31 +11:00
Rafael dos Santos Silva 0ac18d157b FEATURE: Adjustments to gist summaries (#988)
- makes visible to everyone by default
- backfills gists before full summaries
- adds configurable max age setting to backfill job
2024-12-02 15:22:35 -03:00
Keegan George 50ad5415ff UX: no-text style being incorrectly added to tag suggester (#987)
In the tag suggester menu we use `DButton` as a wrapper element and use the `discourseTag` helper to render the text inside the element. So visually there is text content inside the button. However, since `DButton` assumes that no `label`/`translatedLabel` inside an element means `.no-text` CSS style should be applied to the button's element, it was resulting in some incorrect styling being applied to this menu. This PR resolves that by programmatically adding the tag as a `translatedLabel` and then visually hiding it with CSS.
2024-12-02 10:11:04 -08:00
Tobias Eigen c53363406d changed "automatic auto" to "auto" in client.en.yml (#986)
The description of the automatic captions dialog prompt included "Would you like to enable automatic auto captions on image uploads." I removed the second "auto" at the suggestion of translators.
2024-12-02 09:32:08 -08:00
Rafael dos Santos Silva 3828370679 DEV: Cleanup deprecations (#952) 2024-12-02 14:18:03 -03:00
Roman Rizzi 0abd4b1244 FIX: Sentiment classification results needs to be transformed before saving (#983) 2024-11-29 17:31:56 -03:00
David Taylor 120a20c5cd DEV: Convert to native class syntax (#981) 2024-11-29 16:41:09 +01:00
Sam 0cb2c413ba FEATURE: exclude muted categories from category suggester (#979)
The logic here is that users do not particularly care about
topics in the category so we can exclude them from tag
and category suggestions
2024-11-29 12:17:28 +11:00
Rafael dos Santos Silva 80adefa1c1 FIX: Move count logic to the end for tag suggestions (#978) 2024-11-28 16:27:38 -03:00
Sam bc0657f478 FEATURE: AI Usage page (#964)
- Added a new admin interface to track AI usage metrics, including tokens, features, and models.
- Introduced a new route `/admin/plugins/discourse-ai/ai-usage` and supporting API endpoint in `AiUsageController`.
- Implemented `AiUsageSerializer` for structuring AI usage data.
- Integrated CSS stylings for charts and tables under `stylesheets/modules/llms/common/usage.scss`.
- Enhanced backend with `AiApiAuditLog` model changes: added `cached_tokens` column  (implemented with OpenAI for now) with relevant DB migration and indexing.
- Created `Report` module for efficient aggregation and filtering of AI usage metrics.
- Updated AI Bot title generation logic to log correctly to user vs bot
- Extended test coverage for the new tracking features, ensuring data consistency and access controls.
2024-11-29 06:26:48 +11:00
Roman Rizzi c980c34d77 REFACTOR: Simplify sentiment classification (#977)
This change adds a simpler class for sentiment classification, replacing the soon-to-be removed `Classificator` hierarchy. Additionally, it adds a method for classifying concurrently, speeding up the backfill rake task.
2024-11-28 15:38:23 -03:00
Rafael dos Santos Silva 6456a4f44a FIX: Keep related topics data cached when a user scrolls up (#976)
See https://meta.discourse.org/t/related-topics-missing-after-rereading-a-topic-with-more-than-20-posts/306147/6
2024-11-28 14:25:15 -03:00
Rafael dos Santos Silva eacc4ed945 UX: Fix styling of AI Helper on quick edit (#975) 2024-11-28 13:06:48 -03:00
David Taylor 4a2369356f DEV: Update linting (#974) 2024-11-28 16:07:42 +01:00
Rafael dos Santos Silva 4980a4b2f7 FIX: Multiple concurrent summaries could result in pg index errors (#973) 2024-11-28 11:53:04 -03:00
Keegan George 54ebbbe769 UX: Ensure suggestions are left aligned (#971)
This PR ensures that suggestions are aligned to the left. By default, they are center aligned which looks odd when there are long lines of text.
2024-11-27 20:53:30 -08:00
Keegan George 8b33ba3d19 FIX: Suggestion menus not showing in mobile (#970)
The suggestion menu z-index was not enough for mobile so it isn't being shown when it's used in `modalForMobile` bottom menus.
2024-11-27 19:51:03 -08:00
Keegan George f1c7ee8624 DEV: Better control what prompts can appear in post/composer (#969)
This PR updates the logic for the location map so it permits only the desired prompts through to the composer/post menu. Anything else won't be shown by default.

This PR also adds relevant tests to prevent regression.
2024-11-27 16:14:21 -08:00
Keegan George 4da033c667 FIX: Double render error with thumbnail suggestions (#968)
This PR fixes a bug where a double render error appears in the logs when thumbnails are suggested
2024-11-27 15:12:27 -08:00
Keegan George dabef02919 DEV: Prevent detect_text_locale from appearing in menus (#967)
### 🔍 Overview
With the recent changes to allow DiscourseAi in the translator plugin, `detect_text_locale` was needed as a CompletionPrompt. However, it is leaking into composer/post helper menus. This PR ensures we don't not show it in those menus.
2024-11-28 09:27:08 +11:00
Sam fbcc8e493a FEATURE: Skip PM scanning in LLM triage by default (#966)
Usually people do not want to scan personal messages. Sometimes
they may. In that case they can enable triage on personal messages.
2024-11-28 09:25:29 +11:00
Keegan George 6b7d7c1179 REFACTOR: Helper suggestions (#914)
This PR adds some updates to the Helper suggestions to improve it's functionality and modernize some of the codebase.
2024-11-27 12:21:03 -08:00
Rafael dos Santos SilvaandRoman Rizzi 0d3e6b2726 FIX: Fix ordering of random post embeddings backfill (#965)
* FIX: Fix ordering of random post embeddings backfill

* fix annotations

---------

Co-authored-by: Roman Rizzi <[email protected]>
2024-11-27 17:01:54 -03:00
Martin Brennan be05e28679 UX: Add table style for LLM list (#963)
Followup 2f7895bb91,
I forgot to commit this minor change
2024-11-27 13:45:01 +10:00
Martin Brennan 2f7895bb91 UX: Applying more admin UI guidelines (#956)
This commit applies further admin UI guidelines, now that they have been more
fleshed out in core, to the AI admin UI:

* Tools
* LLMs
* Personas

The changes include but are not limited to:

* Applying the table CSS classes, for desktop and mobile
* Adding a description and learn more link for each tab
* Adding an empty list placeholder with CTA using `AdminConfigAreaEmptyList`
* Replacing custom headings with `AdminPageSubheader`
2024-11-27 13:34:56 +10:00
Discourse Translator Bot 7a094cde18 Update translations (#962) 2024-11-26 23:15:10 +01:00
Roman Rizzi 251628bfa1 FIX: Shutdown embeddings thread pool after processing (#961) 2024-11-26 18:12:03 -03:00
Roman Rizzi ef07fcb308 FIX: Skip records without content to classify (#960) 2024-11-26 15:54:20 -03:00
Roman Rizzi ddf2bf7034 DEV: Backfill embeddings concurrently. (#941)
We are adding a new method for generating and storing embeddings in bulk, which relies on `Concurrent::Promises::Future`. Generating an embedding consists of three steps:

Prepare text
HTTP call to retrieve the vector
Save to DB.
Each one is independently executed on whatever thread the pool gives us.

We are bringing a custom thread pool instead of the global executor since we want control over how many threads we spawn to limit concurrency. We also avoid firing thousands of HTTP requests when working with large batches.
2024-11-26 14:12:32 -03:00
Rafael dos Santos Silva 23193ee6f2 FEATURE: Calculate gists from non hot topics too (#958)
Also renames some settings to remove 'hot' references.
2024-11-26 13:44:12 -03:00
Sam 54f2d34ccb DEV: skip flakey spec (#955)
This spec fails inconsistently with:

-fragment-n14
       +You are a helpful Discourse assistant.
       +You _understand_ and **generate** Discourse Markdown.
       +You live in a Discourse Forum Message.
       +
       +You live in the forum with the URL: http://test.localhost
       +The title of your site: test site title
       +The description is: test site description
       +The participants in this conversation are: joe, jane
       +The date now is: 2024-11-25 20:23:02 UTC, much has changed since you were trained.
       +
       +You were trained on OLD data, lean on search to get up to date information about this forum
       +When searching try to SIMPLIFY search terms
       +Discourse search joins all terms with AND. Reduce and simplify terms to find more results.<guidance>
       +The following texts will give you additional guidance for your response.
       +We included them because we believe they are relevant to this conversation topic.
       +
       +Texts:
       +
       +fragment-n10
       +fragment-n9
       +fragment-n8
       +fragment-n7
       +fragment-n6
       +fragment-n5
       +fragment-n4
       +fragment-n3
       +fragment-n2
       +fragment-n1
       +</guidance>
2024-11-26 07:49:33 +11:00
Sam 616b990894 FEATURE: LLM mentions and auto silence (#949)
* FEATURE: allow mentioning an LLM mid conversation to switch

This is a edgecase feature that allow you to start a conversation
in a PM with LLM1 and then use LLM2 to evaluation or continue
the conversation

* FEATURE: allow auto silencing of spam accounts

New rule can also allow for silencing an account automatically

This can prevent spammers from creating additional posts.
2024-11-26 07:19:56 +11:00
356 changed files with 13701 additions and 4229 deletions
@@ -1,19 +1,19 @@
import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({
export default class AdminPluginsShowDiscourseAiLlmsEdit extends DiscourseRoute {
async model(params) {
const allLlms = this.modelFor("adminPlugins.show.discourse-ai-llms");
const id = parseInt(params.id, 10);
const record = allLlms.findBy("id", id);
record.provider_params = record.provider_params || {};
return record;
},
}
setupController(controller, model) {
this._super(controller, model);
super.setupController(controller, model);
controller.set(
"allLlms",
this.modelFor("adminPlugins.show.discourse-ai-llms")
);
},
});
}
}
@@ -1,18 +1,18 @@
import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({
queryParams: {
export default class AdminPluginsShowDiscourseAiLlmsNew extends DiscourseRoute {
queryParams = {
llmTemplate: { refreshModel: true },
},
};
async model() {
const record = this.store.createRecord("ai-llm");
record.provider_params = {};
return record;
},
}
setupController(controller, model) {
this._super(controller, model);
super.setupController(controller, model);
controller.set(
"allLlms",
this.modelFor("adminPlugins.show.discourse-ai-llms")
@@ -21,5 +21,5 @@ export default DiscourseRoute.extend({
"llmTemplate",
this.paramsFor(this.routeName).llmTemplate || null
);
},
});
}
}
@@ -1,19 +1,19 @@
import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({
export default class AdminPluginsShowDiscourseAiPersonasEdit extends DiscourseRoute {
async model(params) {
const allPersonas = this.modelFor(
"adminPlugins.show.discourse-ai-personas"
);
const id = parseInt(params.id, 10);
return allPersonas.findBy("id", id);
},
}
setupController(controller, model) {
this._super(controller, model);
super.setupController(controller, model);
controller.set(
"allPersonas",
this.modelFor("adminPlugins.show.discourse-ai-personas")
);
},
});
}
}
@@ -1,7 +1,7 @@
import { AUTO_GROUPS } from "discourse/lib/constants";
import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({
export default class AdminPluginsShowDiscourseAiPersonasNew extends DiscourseRoute {
async model() {
const record = this.store.createRecord("ai-persona");
record.set("allowed_group_ids", [AUTO_GROUPS.trust_level_0.id]);
@@ -13,13 +13,13 @@ export default DiscourseRoute.extend({
record.set("allow_personal_messages", true);
record.set("tool_details", false);
return record;
},
}
setupController(controller, model) {
this._super(controller, model);
super.setupController(controller, model);
controller.set(
"allPersonas",
this.modelFor("adminPlugins.show.discourse-ai-personas")
);
},
});
}
}
@@ -0,0 +1,11 @@
import { service } from "@ember/service";
import { ajax } from "discourse/lib/ajax";
import DiscourseRoute from "discourse/routes/discourse";
export default class DiscourseAiSpamRoute extends DiscourseRoute {
@service store;
model() {
return ajax("/admin/plugins/discourse-ai/ai-spam.json");
}
}
@@ -1,6 +1,6 @@
import DiscourseRoute from "discourse/routes/discourse";
export default class DiscourseAiToolsShowRoute extends DiscourseRoute {
export default class DiscourseAiToolsEditRoute extends DiscourseRoute {
async model(params) {
const allTools = this.modelFor("adminPlugins.show.discourse-ai-tools");
const id = parseInt(params.id, 10);
@@ -0,0 +1,11 @@
import { service } from "@ember/service";
import { ajax } from "discourse/lib/ajax";
import DiscourseRoute from "discourse/routes/discourse";
export default class DiscourseAiUsageRoute extends DiscourseRoute {
@service store;
model() {
return ajax("/admin/plugins/discourse-ai/ai-usage.json");
}
}
@@ -0,0 +1 @@
<AiSpam @model={{this.model}} />
@@ -0,0 +1,7 @@
<section class="ai-persona-tool-editor__current admin-detail pull-left">
<AiToolEditor
@tools={{this.allTools}}
@model={{this.model}}
@presets={{this.presets}}
/>
</section>
@@ -1,5 +1,7 @@
<AiToolEditor
@tools={{this.allTools}}
@model={{this.model}}
@presets={{this.presets}}
/>
<section class="ai-persona-tool-editor__current admin-detail pull-left">
<AiToolEditor
@tools={{this.allTools}}
@model={{this.model}}
@presets={{this.presets}}
/>
</section>
@@ -1,5 +0,0 @@
<AiToolEditor
@tools={{this.allTools}}
@model={{this.model}}
@presets={{this.presets}}
/>
@@ -0,0 +1 @@
<AiUsage @model={{this.model}} />
@@ -30,7 +30,10 @@ module DiscourseAi
}
end
def show
def new
end
def edit
llm_model = LlmModel.find(params[:id])
render json: LlmModelSerializer.new(llm_model)
end
@@ -5,7 +5,7 @@ module DiscourseAi
class AiPersonasController < ::Admin::AdminController
requires_plugin ::DiscourseAi::PLUGIN_NAME
before_action :find_ai_persona, only: %i[show update destroy create_user]
before_action :find_ai_persona, only: %i[edit update destroy create_user]
def index
ai_personas =
@@ -33,7 +33,10 @@ module DiscourseAi
render json: { ai_personas: ai_personas, meta: { tools: tools, llms: llms } }
end
def show
def new
end
def edit
render json: LocalizedAiPersonaSerializer.new(@ai_persona)
end
@@ -0,0 +1,112 @@
# frozen_string_literal: true
module DiscourseAi
module Admin
class AiSpamController < ::Admin::AdminController
requires_plugin "discourse-ai"
def show
render json: AiSpamSerializer.new(spam_config, root: false)
end
def update
updated_params = {}
if allowed_params.key?(:llm_model_id)
llm_model_id = updated_params[:llm_model_id] = allowed_params[:llm_model_id]
if llm_model_id.to_i < 0 &&
!SiteSetting.ai_spam_detection_model_allowed_seeded_models_map.include?(
"custom:#{llm_model_id}",
)
return(
render_json_error(
I18n.t("discourse_ai.llm.configuration.invalid_seeded_model"),
status: 422,
)
)
end
end
updated_params[:data] = {
custom_instructions: allowed_params[:custom_instructions],
} if allowed_params.key?(:custom_instructions)
if updated_params.present?
# not using upsert cause we will not get the correct validation errors
if AiModerationSetting.spam
AiModerationSetting.spam.update!(updated_params)
else
AiModerationSetting.create!(updated_params.merge(setting_type: :spam))
end
end
is_enabled = ActiveModel::Type::Boolean.new.cast(allowed_params[:is_enabled])
if allowed_params.key?(:is_enabled)
if is_enabled && !AiModerationSetting.spam&.llm_model_id
return(
render_json_error(
I18n.t("discourse_ai.llm.configuration.must_select_model"),
status: 422,
)
)
end
SiteSetting.ai_spam_detection_enabled = is_enabled
end
render json: AiSpamSerializer.new(spam_config, root: false)
end
def test
url = params[:post_url].to_s
post = nil
if url.match?(/^\d+$/)
post_id = url.to_i
post = Post.find_by(id: post_id)
end
route = UrlHelper.rails_route_from_url(url) if !post
if route
if route[:controller] == "topics"
post_number = route[:post_number] || 1
post = Post.with_deleted.find_by(post_number: post_number, topic_id: route[:topic_id])
end
end
raise Discourse::NotFound if !post
result =
DiscourseAi::AiModeration::SpamScanner.test_post(
post,
custom_instructions: params[:custom_instructions],
llm_id: params[:llm_id],
)
render json: result
end
private
def allowed_params
params.permit(:is_enabled, :llm_model_id, :custom_instructions)
end
def spam_config
spam_config = {
enabled: SiteSetting.ai_spam_detection_enabled,
settings: AiModerationSetting.spam,
}
spam_config[:stats] = DiscourseAi::AiModeration::SpamReport.generate(min_date: 1.week.ago)
if spam_config[:stats].scanned_count > 0
spam_config[
:flagging_username
] = DiscourseAi::AiModeration::SpamScanner.flagging_user&.username
end
spam_config
end
end
end
end
@@ -5,14 +5,17 @@ module DiscourseAi
class AiToolsController < ::Admin::AdminController
requires_plugin ::DiscourseAi::PLUGIN_NAME
before_action :find_ai_tool, only: %i[test show update destroy]
before_action :find_ai_tool, only: %i[test edit update destroy]
def index
ai_tools = AiTool.all
render_serialized({ ai_tools: ai_tools }, AiCustomToolListSerializer, root: false)
end
def show
def new
end
def edit
render_serialized(@ai_tool, AiCustomToolSerializer)
end
@@ -0,0 +1,30 @@
# frozen_string_literal: true
module DiscourseAi
module Admin
class AiUsageController < ::Admin::AdminController
requires_plugin "discourse-ai"
def show
end
def report
render json: AiUsageSerializer.new(create_report, root: false)
end
private
def create_report
report =
DiscourseAi::Completions::Report.new(
start_date: params[:start_date]&.to_date || 30.days.ago,
end_date: params[:end_date]&.to_date || Time.current,
)
report = report.filter_by_feature(params[:feature]) if params[:feature].present?
report = report.filter_by_model(params[:model]) if params[:model].present?
report
end
end
end
end
@@ -19,22 +19,33 @@ module DiscourseAi
raise Discourse::NotFound if !guardian.can_see?(post)
end
name = artifact.name
if params[:version].present?
artifact = artifact.versions.find_by(version_number: params[:version])
raise Discourse::NotFound if !artifact
end
js = artifact.js || ""
if !js.match?(%r{\A\s*<script.*</script>}mi)
mod = ""
mod = " type=\"module\"" if js.match?(/\A\s*import.*/)
js = "<script#{mod}>\n#{js}\n</script>"
end
# Prepare the inner (untrusted) HTML document
untrusted_html = <<~HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>#{ERB::Util.html_escape(artifact.name)}</title>
<title>#{ERB::Util.html_escape(name)}</title>
<style>
#{artifact.css}
</style>
</head>
<body>
#{artifact.html}
<script>
#{artifact.js}
</script>
#{js}
</body>
</html>
HTML
@@ -45,7 +56,7 @@ module DiscourseAi
<html>
<head>
<meta charset="UTF-8">
<title>#{ERB::Util.html_escape(artifact.name)}</title>
<title>#{ERB::Util.html_escape(name)}</title>
<style>
html, body, iframe {
margin: 0;
@@ -67,7 +78,9 @@ module DiscourseAi
HTML
response.headers.delete("X-Frame-Options")
response.headers["Content-Security-Policy"] = "script-src 'unsafe-inline';"
response.headers[
"Content-Security-Policy"
] = "script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://unpkg.com https://cdnjs.cloudflare.com https://ajax.googleapis.com https://cdn.jsdelivr.net;"
response.headers["X-Robots-Tag"] = "noindex"
# Render the content
@@ -35,7 +35,7 @@ module DiscourseAi
prompt.custom_instruction = params[:custom_prompt]
end
suggest_thumbnails(input) if prompt.id == CompletionPrompt::ILLUSTRATE_POST
return suggest_thumbnails(input) if prompt.id == CompletionPrompt::ILLUSTRATE_POST
hijack do
render json:
+13
View File
@@ -0,0 +1,13 @@
# frozen_string_literal: true
module Jobs
class AiSpamScan < ::Jobs::Base
def execute(args)
return if !args[:post_id]
post = Post.find_by(id: args[:post_id])
return if !post
DiscourseAi::AiModeration::SpamScanner.perform_scan(post)
end
end
end
+1 -3
View File
@@ -18,9 +18,7 @@ module ::Jobs
target = target_type.constantize.find_by(id: target_id)
return if !target
truncation = DiscourseAi::Embeddings::Strategies::Truncation.new
vector_rep =
DiscourseAi::Embeddings::VectorRepresentations::Base.current_representation(truncation)
vector_rep = DiscourseAi::Embeddings::VectorRepresentations::Base.current_representation
tokenizer = vector_rep.tokenizer
chunk_tokens = target.rag_chunk_tokens
-17
View File
@@ -1,17 +0,0 @@
# frozen_string_literal: true
module Jobs
class EvaluatePostUploads < ::Jobs::Base
def execute(args)
return unless SiteSetting.ai_nsfw_detection_enabled
return if (post_id = args[:post_id]).blank?
post = Post.includes(:uploads).find_by_id(post_id)
return if post.nil? || post.uploads.empty?
return if post.uploads.none? { |u| FileHelper.is_supported_image?(u.url) }
DiscourseAi::PostClassificator.new(DiscourseAi::Nsfw::Classification.new).classify!(post)
end
end
end
@@ -1,23 +1,20 @@
# frozen_string_literal: true
module ::Jobs
class UpdateHotTopicGist < ::Jobs::Base
class FastTrackTopicGist < ::Jobs::Base
sidekiq_options retry: false
def execute(args)
return if !SiteSetting.discourse_ai_enabled
return if !SiteSetting.ai_summarization_enabled
return if SiteSetting.ai_summarize_max_hot_topics_gists_per_batch.zero?
return if !SiteSetting.ai_summary_gists_enabled
topic = Topic.find_by(id: args[:topic_id])
return if topic.blank?
return if !TopicHotScore.where(topic: topic).exists?
summarizer = DiscourseAi::Summarization.topic_gist(topic)
gist = summarizer.existing_summary
return if gist.blank?
return if !gist.outdated
return if gist.present? && (!gist.outdated || gist.created_at >= 5.minutes.ago)
summarizer.force_summarize(Discourse.system_user)
end
+1 -5
View File
@@ -16,11 +16,7 @@ module Jobs
return if topic.private_message? && !SiteSetting.ai_embeddings_generate_for_pms
return if post.raw.blank?
strategy = DiscourseAi::Embeddings::Strategies::Truncation.new
vector_rep =
DiscourseAi::Embeddings::VectorRepresentations::Base.current_representation(strategy)
vector_rep.generate_representation_from(target)
DiscourseAi::Embeddings::Vector.instance.generate_representation_from(target)
end
end
end
+2 -4
View File
@@ -8,13 +8,11 @@ module ::Jobs
def execute(args)
return if (fragments = RagDocumentFragment.where(id: args[:fragment_ids].to_a)).empty?
truncation = DiscourseAi::Embeddings::Strategies::Truncation.new
vector_rep =
DiscourseAi::Embeddings::VectorRepresentations::Base.current_representation(truncation)
vector = DiscourseAi::Embeddings::Vector.instance
# generate_representation_from checks compares the digest value to make sure
# the embedding is only generated once per fragment unless something changes.
fragments.map { |fragment| vector_rep.generate_representation_from(fragment) }
fragments.map { |fragment| vector.generate_representation_from(fragment) }
last_fragment = fragments.last
target = last_fragment.target
-22
View File
@@ -1,22 +0,0 @@
# frozen_string_literal: true
module ::Jobs
class HotTopicsGistBatch < ::Jobs::Base
def execute(_args)
return if !SiteSetting.discourse_ai_enabled
return if !SiteSetting.ai_summarization_enabled
return if SiteSetting.ai_summarize_max_hot_topics_gists_per_batch.zero?
Topic
.joins("JOIN topic_hot_scores on topics.id = topic_hot_scores.topic_id")
.order("topic_hot_scores.score DESC")
.limit(SiteSetting.ai_summarize_max_hot_topics_gists_per_batch)
.each do |topic|
summarizer = DiscourseAi::Summarization.topic_gist(topic)
gist = summarizer.existing_summary
summarizer.force_summarize(Discourse.system_user) if gist.blank? || gist.outdated
end
end
end
end
+1 -3
View File
@@ -9,9 +9,7 @@ module ::Jobs
post = Post.find_by(id: post_id, post_type: Post.types[:regular])
return if post&.raw.blank?
DiscourseAi::PostClassificator.new(
DiscourseAi::Sentiment::SentimentClassification.new,
).classify!(post)
DiscourseAi::Sentiment::PostClassification.new.classify!(post)
end
end
end
@@ -1,18 +0,0 @@
# frozen_string_literal: true
module ::Jobs
class ToxicityClassifyChatMessage < ::Jobs::Base
def execute(args)
return unless SiteSetting.ai_toxicity_enabled
return if (chat_message_id = args[:chat_message_id]).blank?
chat_message = ::Chat::Message.find_by(id: chat_message_id)
return if chat_message&.message.blank?
DiscourseAi::ChatMessageClassificator.new(
DiscourseAi::Toxicity::ToxicityClassification.new,
).classify!(chat_message)
end
end
end

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