Compare commits

..
Author SHA1 Message Date
Keegan George 3ce8d943a7 DEV: First version
showing message bus error, but it kinda works? wip on a fix.
2024-11-12 20:42:19 -08:00
Sam 9551b1a4d1 FIX: do not strip empty string during stream processing (#911)
Fixes issue in Open AI provider eating newlines and spaces
2024-11-13 07:12:00 +11:00
Rafael dos Santos Silva aef9a03d4c FEATURE: Truncate AI Captions to a reasonable max size (#907) 2024-11-12 15:52:46 -03:00
Sérgio Saquetim 9583964676 DEV: Added compatibility with the Glimmer Post Menu (#887) 2024-11-12 15:46:17 -03:00
Keegan George 2fc05685bb DEV: Apply text formatting and AI feature naming conventions (#905)
This PR applies Discourse's formatting text guidelines as per [documentation on Meta](https://meta.discourse.org/t/formatting-text-in-discourse-documentation-and-uis/324637). Additionally, applies our AI feature naming conventions, standardizing feature names.
2024-11-12 08:48:30 -08:00
Kris 26bfda4763 UX: reduce topic list title size when gists are enabled (#910) 2024-11-12 09:55:07 -05:00
Discourse Translator Bot 6ef635619f Update translations (#909) 2024-11-12 14:54:47 +01:00
Sam e817b7dc11 FEATURE: improve tool support (#904)
This re-implements tool support in DiscourseAi::Completions::Llm #generate

Previously tool support was always returned via XML and it would be the responsibility of the caller to parse XML

New implementation has the endpoints return ToolCall objects.

Additionally this simplifies the Llm endpoint interface and gives it more clarity. Llms must implement

decode, decode_chunk (for streaming)

It is the implementers responsibility to figure out how to decode chunks, base no longer implements. To make this easy we ship a flexible json decoder which is easy to wire up.

Also (new)

    Better debugging for PMs, we now have a next / previous button to see all the Llm messages associated with a PM
    Token accounting is fixed for vllm (we were not correctly counting tokens)
2024-11-12 08:14:30 +11:00
Keegan George 644141ff08 FIX: Regenerate summary button still shows cached summary (#903)
This PR fixes an issue where clicking to regenerate a summary was still showing the cached summary. To resolve this we call resetSummary() to reset all the summarization related properties before creating a new request.
2024-11-07 16:01:18 -08:00
Roman Rizzi fbc74c7467 FEATURE: Extend summary backfill to also generate gists (#896)
Updates default batch size to 0 and max to 10000
2024-11-07 13:40:18 -03:00
Keegan George c421f713a3 DEV: Handle streaming animation within AiSummaryBox (#901)
This PR further decouples the streaming animation by completely handling the streaming animation directly in the `AiSummaryBox` component. Previously, handling the streaming animation by calling methods in the `ai-streamer` API was leading to timing issues making things out-of-sync. This results in some issues such as the last update of streamed text not being shown. Handling streaming directly in the component should simplify things drastically and prevent any issues.
2024-11-07 08:08:32 -08:00
Rafael dos Santos Silva 021e09607d FIX: Unhide gemini api key setting for embeddings (#902) 2024-11-07 11:55:18 -03:00
Kris 893fa624e4 UX: increase gist size, and adjust surrounding elements to accommodate (#900) 2024-11-07 08:35:05 -05:00
Kris 1ad5321c09 UX: add sparkle icon to related topics for anons (#897) 2024-11-05 17:15:20 -05:00
Osama Sayegh da7c97d294 FIX: Specify type for Rag upload (#895)
Specifying `type` when using `UppyUpload` will be required as of https://github.com/discourse/discourse/pull/29600.
2024-11-05 22:10:54 +03:00
Discourse Translator Bot ab4678275d Update translations (#894) 2024-11-05 16:55:54 +01:00
Keegan George 99282612a9 DEV: Prefer ENV key for seeded models (#893)
This PR ensures we prefer getting the API key from environment variables when it is a seeded model.
2024-11-05 06:19:13 -08:00
Roman Rizzi 9505a8976c FEATURE: Automatically backfill regular summaries. (#892)
This change introduces a job to summarize topics and cache the results automatically. We provide a setting to control how many topics we'll backfill per hour and what the topic's minimum word count is to qualify.

We'll prioritize topics without summary over outdated ones.
2024-11-04 17:48:11 -03:00
Sam 98022d7d96 FEATURE: support custom instructions for persona streaming (#890)
This allows us to inject information into the system prompt
which can help shape replies without repeating over and over
in messages.
2024-11-05 07:43:26 +11:00
Jarek Radosz fa7ca8bc31 DEV: Use the new more-topics API (#885)
See: https://github.com/discourse/discourse/pull/29143
2024-11-04 17:42:50 +01:00
Rafael dos Santos Silva 772ee934ab Migrate sentiment to a TEI backend (#886) 2024-11-04 09:14:34 -03:00
Sam bffe9dfa07 FIX: we must properly encode objects prior to escaping (#891)
in cases of arrays escapeHTML will not work)

*
2024-11-04 16:16:25 +11:00
Sam c352054d4e FIX: encode parameters returned from LLMs correctly (#889)
Fixes encoding of params on LLM function calls.

Previously we would improperly return results if a function parameter returned an HTML tag.

Additionally adds some missing HTTP verbs to tool calls.
2024-11-04 10:07:17 +11:00
Roman Rizzi 7e3a543f6f FEATURE: Double gist length to 40 words (#888) 2024-11-01 13:09:03 -03:00
Kris 32ea421408 UX: in share, use native image dimensions and hide filename (#880) 2024-10-31 13:51:10 -04:00
Roman Rizzi e8f0633141 DEV: Extend truncation to all summarizable content (#884) 2024-10-31 12:17:42 -03:00
Roman Rizzi e8eed710e0 FIX: Truncate OP for gists to help the model focus on the latest posts (#883) 2024-10-31 10:54:56 -03:00
Roman Rizzi 32fb023357 COPY: Include model names in sentiment report descriptions (#882) 2024-10-30 15:50:28 -03:00
Roman Rizzi 00e4a84305 COPY: Update sentiment report descriptions to clarify how it works (#881) 2024-10-30 11:48:32 -03:00
Sam 34a59b623e FIX: ensure replies are never double streamed (#879)
The custom field "discourse_ai_bypass_ai_reply" was added so
we can signal the post created hook to bypass replying even
if it thinks it should.

Otherwise there are cases where we double answer user questions
leading to much confusion.

This also slightly refactors code making the controller smaller
2024-10-30 20:24:39 +11:00
be0b78cacd FEATURE: new endpoint for directly accessing a persona (#876)
The new `/admin/plugins/discourse-ai/ai-personas/stream-reply.json` was added.

This endpoint streams data direct from a persona and can be used
to access a persona from remote systems leaving a paper trail in
PMs about the conversation that happened

This endpoint is only accessible to admins.

---------

Co-authored-by: Gabriel Grubba <[email protected]>
Co-authored-by: Keegan George <[email protected]>
2024-10-30 10:28:20 +11:00
Kris 05790a6a40 UX: convert AI gist disclosure to a toggle (#878) 2024-10-29 11:59:41 -04:00
Discourse Translator Bot e7a66b0789 Update translations (#877) 2024-10-29 15:31:51 +01:00
Roman Rizzi dd404c924a DEV: Use different feature_names for summarization strategies (#875) 2024-10-29 08:45:14 -03:00
dependabot[bot] 0f0f2a247a Build(deps-dev): Bump rexml from 3.3.6 to 3.3.9 (#874)
Bumps [rexml](https://github.com/ruby/rexml) from 3.3.6 to 3.3.9.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.3.6...v3.3.9)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-28 20:44:39 +01:00
Roman Rizzi 37b6461d68 FIX: Make sure that topic gists are displayed ONLY on the hot list. (#873) 2024-10-28 15:15:53 -03:00
Rafael dos Santos Silva 820b506910 DEV: Hide soon to be deprecated modules settings (#872) 2024-10-28 14:27:25 -03:00
David Taylor 945f04b089 DEV: Update plugin annotations (#871) 2024-10-28 14:07:09 +00:00
Bianca Nenciu 294c364a75 DEV: Fix mismatched column types (#868)
The primary key is usually a bigint column, but the foreign key columns
are usually of integer type. This can lead to issues when joining these
columns due to mismatched types and different value ranges.

This was using a temporary plugin / test API to make tests pass, but it
is safe to alter "ai_document_fragment_embeddings" and
"rag_document_fragments" tables because they usually have less than 1M
rows and migration is going to be fast.

Depending on the size of the community, "classification_results" table
may have more than 1M rows and the migration will lock the table for a
longer time. However, classification runs in background jobs and they
will be automatically retried if they fail due to the lock, which makes
it acceptable.
2024-10-28 15:36:42 +02:00
jbrw c479b177a7 DEV: Check for presence of currentRoute.attributes (#870) 2024-10-25 13:30:29 -04:00
Rafael dos Santos Silva 8ded4b2e58 FIX: Use present? instead of invalid exists? (#869) 2024-10-25 13:04:42 -03:00
Roman RizziandRafael Silva a2b1ea3c63 FEATURE: Fast-track gist regeneration when a hot topic gets a new post (#860)
* FEATURE: Fast-track gist regeneration when a hot topic gets a new post

* DEV: Introduce an upsert-like summarize

* FIX: Only enqueue fast-track gist for hot hot hot topics

---------

Co-authored-by: Rafael Silva <[email protected]>
2024-10-25 12:38:49 -03:00
Rafael dos Santos SilvaandRoman Rizzi 33da27e231 FIX: Change hot gist prompt to avoid title repeating #859 (#859)
Co-authored-by: Roman Rizzi <[email protected]>
2024-10-25 12:12:33 -03:00
Roman Rizzi ec97996905 FIX/REFACTOR: FoldContent revamp (#866)
* FIX/REFACTOR: FoldContent revamp

We hit a snag with our hot topic gist strategy: the regex we used to split the content didn't work, so we cannot send the original post separately. This was important for letting the model focus on what's new in the topic.

The algorithm doesn’t give us full control over how prompts are written, and figuring out how to format the content isn't straightforward. This means we're having to use more complicated workarounds, like regex.

To tackle this, I'm suggesting we simplify the approach a bit. Let's focus on summarizing as much as we can upfront, then gradually add new content until there's nothing left to summarize.

Also, the "extend" part is mostly for models with small context windows, which shouldn't pose a problem 99% of the time with the content volume we're dealing with.

* Fix fold docs

* Use #shift instead of #pop to get the first elem, not the last
2024-10-25 11:51:17 -03:00
Sam 12869f2146 FIX: testing tool was not showing rag results (#867)
This changeset contains 4 fixes:

1. We were allowing running tests on unsaved tools,
this is problematic cause uploads are not yet associated or indexed
leading to confusing results. We now only show the test button when
tool is saved.


2. We were not properly scoping rag document fragements, this
meant that personas and ai tools could get results from other
unrelated tools, just to be filtered out later


3. index.search showed options as "optional" but implementation
required the second option

4. When testing tools searching through document fragments was
not working at all cause we did not properly load the tool
2024-10-25 16:01:25 +11:00
Sam 4923837165 FIX: Llm selector / forced tools / search tool (#862)
* FIX: Llm selector / forced tools / search tool


This fixes a few issues:

1. When search was not finding any semantic results we would break the tool
2. Gemin / Anthropic models did not implement forced tools previously despite it being an API option
3. Mechanics around displaying llm selector were not right. If you disabled LLM selector server side persona PM did not work correctly.
4. Disabling native tools for anthropic model moved out of a site setting. This deliberately does not migrate cause this feature is really rare to need now, people who had it set probably did not need it.
5. Updates anthropic model names to latest release

* linting

* fix a couple of tests I missed

* clean up conditional
2024-10-25 06:24:53 +11:00
Rafael dos Santos Silva 3022d34613 FEATURE: Support srv records for OpenAI compatible LLMs (#865) 2024-10-24 15:47:12 -03:00
David Taylor c1fa84ad29 DEV: Update form-template-field/upload uppy usage (#863) 2024-10-24 15:21:13 +01:00
Kris fc6f0a6560 UX: minor gist optimizations for readability (#864) 2024-10-24 09:31:52 -04:00
Keegan George 9e8608b070 UX: Hide AI bot in seeded LLM (#858)
AI bot won't be turned on for seeded LLMs so it makes no sense to expose it here. This will cleanup the template and avoid the double `{{#unless}}` check.
2024-10-23 16:36:17 -07:00
Kris 72111a10ae UX: add disclosure for topic list gists (#861) 2024-10-23 19:32:22 -04:00
Rafael dos Santos Silva 96f5f8cbd0 FIX: Basic cleanup of AI Caption to remove line breaks and pipes (#857) 2024-10-23 18:38:29 -03:00
Keegan George 9af0c2e719 UX: Improve seeded LLM edit page (#856) 2024-10-23 13:58:27 -07:00
Kris 0aa2789437 UX: switch gist outlet to avoid badges (#855) 2024-10-23 15:10:41 -04:00
Kris 657d103919 UX: adjust gist position, darken unread color (#854) 2024-10-23 13:00:52 -04:00
Sam f1283e156d FEATURE: allow scoping of google tool queries (#852)
This allows to simply scope search results to specific domains and prepend arbitrary snippets to searches made
2024-10-23 16:55:10 +11:00
Sam 059d3b6fd2 FEATURE: better logging for automation reports (#853)
A new feature_context json column was added to ai_api_audit_logs

This allows us to store rich json like context on any LLM request
made.

This new field now stores automation id and name.

Additionally allows llm_triage to specify maximum number of tokens

This means that you can limit the cost of llm triage by scanning only
first N tokens of a post.
2024-10-23 16:49:56 +11:00
Keegan George eae7716177 DEV: Improve ai-streamer API (#851)
In preparation for applying the streaming animation elsewhere, we want to better improve the organization of folder structure and methods used in the `ai-streamer`
2024-10-22 10:55:35 -07:00
Discourse Translator Bot b604ff9a23 Update translations (#850) 2024-10-22 22:44:42 +09:00
Sam c6276be63e FIX: add missing translation (#849) 2024-10-22 12:10:08 +11:00
Sam a1f859a415 FEATURE: improve visibility of AI usage in LLM page (#845)
This changeset: 

1. Corrects some issues with "force_default_llm" not applying
2. Expands the LLM list page to show LLM usage
3. Clarifies better what "enabling a bot" on an llm means (you get it in the selector)
2024-10-22 11:16:02 +11:00
Keegan George 712a07c39b DEV: IMAGE_MARKDOWN_REGEX import available in core (#847) 2024-10-21 14:48:10 -07:00
Roman Rizzi 3533814870 UX: Avoid introductory phrases and summarize topics without replies (#848) 2024-10-21 17:53:48 -03:00
Roman Rizzi 6d504ab80d FEATURE: Make hot topic gists opt-in. (#846)
This change restricts gists to members of specific groups. It also fixes a bug where other lists could display the gist if available.
2024-10-21 15:15:25 -03:00
Keegan George 37c2930fbf FIX: Decouple DOM manipulation from SummaryStreamer (#844)
Previously, when we added smooth streaming animation to summarization (https://github.com/discourse/discourse-ai/pull/778) we used the same logic and lib we did for AI Bot. However, since `AiSummaryBox` is an Ember component, the direct DOM manipulation done in the streamer (`SummaryUpdater`) would often result in issues with summarization where sometimes summarization updates would hang, especially on the last result. This is likely due to the DOM manipulation being done in the streamer being incongruent with Ember's way of rendering.

In this PR, we remove the direct DOM manipulation done in the lib `SummaryUpdater` in favour of directly updating the properties in `AiSummaryBox` using the `componentContext`. Instead of messing with Ember's rendered DOM, passing the updates and allowing the component to render the updates directly should likely prevent further issues with summarization.

The bug itself is quite difficult to repro and also difficult to test, so no tests have been added to this PR. But I will be manually testing and assessing for any potential issues.
2024-10-21 09:15:25 -07:00
Roman Rizzi e768fa877e FIX: Don't regenerate up to date gists (#843) 2024-10-18 18:49:01 -03:00
Roman RizziandRafael dos Santos Silva 27b5542357 FEATURE: Generate topic gists for the hot topics list. (#837)
* Display gists in the hot topics list

* Adjust hot topics gist strategy and add a job to generate gists

* Replace setting with a configurable batch size

* Avoid loading summaries for other topic lists

* Tweak gist prompt to focus on latest posts in the context of the OP

* Remove serializer hack and rely on core change from discourse/discourse#29291

* Update lib/summarization/strategies/hot_topic_gists.rb

Co-authored-by: Rafael dos Santos Silva <[email protected]>

---------

Co-authored-by: Rafael dos Santos Silva <[email protected]>
2024-10-18 18:01:39 -03:00
Rafael dos Santos Silvaand耗子 decf1bb49d FIX: api key header error (#839)
* FIX: api key header error

* FIX: remove unnecessary headers

* FIX: a error

* FEATURE: Add both headers

---------

Co-authored-by: 耗子 <[email protected]>
2024-10-16 15:57:36 -03:00
Kris 25cc03809a DEV: ensure far-copy icon is included in subset (#841) 2024-10-16 12:32:13 -04:00
Loïc Guitaut 7919173cba DEV: Use ChatSDK.create instead of service in specs
This patch will allow upcoming changes to services
(https://github.com/discourse/discourse/pull/29129) without breaking the
`discourse-ai` specs.
2024-10-16 18:18:32 +02:00
Rafael dos Santos Silva 792703c942 FEATURE: Discord Bot integration (#831)
This adds support for the a Discord bot that can search in a Discourse instance when invoked via slash commands in Discord Guild channel.
2024-10-16 12:41:18 -03:00
Discourse Translator Bot 3432f6654f Update translations (#836) 2024-10-16 17:52:11 +09:00
Martin Brennan d7745d1ac3 FIX: Remove missed AiPersona.allowed_chat (#838)
Followup bdf3b6268b

I think this is the fix but not sure how to test it,
this is breaking the build
2024-10-16 10:33:24 +11:00
Sam bdf3b6268b FEATURE: smarter persona tethering (#832)
Splits persona permissions so you can allow a persona on:

- chat dms
- personal messages
- topic mentions
- chat channels

(any combination is allowed)

Previously we did not have this flexibility.

Additionally, adds the ability to "tether" a language model to a persona so it will always be used by the persona. This allows people to use a cheaper language model for one group of people and more expensive one for other people
2024-10-16 07:20:31 +11:00
Roman RizziandPenar Musaraj c7acb4a6a0 REFACTOR: Support of different summarization targets/prompts. (#835)
* DEV: Add summary types

* Refactor for different summary types

* Use enum for summary types

* Update lib/summarization/strategies/topic_summary.rb

Co-authored-by: Penar Musaraj <[email protected]>

* Update lib/summarization/strategies/topic_gist.rb

Co-authored-by: Penar Musaraj <[email protected]>

* Update lib/summarization/strategies/chat_messages.rb

Co-authored-by: Penar Musaraj <[email protected]>

* Fix chat_messages single prompt

* Small tweak to the chat summarization prompt

---------

Co-authored-by: Penar Musaraj <[email protected]>
2024-10-15 13:53:26 -03:00
Rafael dos Santos SilvaandRoman Rizzi 791fad1e6a FEATURE: Index embeddings using bit vectors (#824)
On very large sites, the rare cache misses for Related Topics can take around 200ms, which affects our p99 metric on the topic page. In order to mitigate this impact, we now have several tools at our disposal.

First, one is to migrate the index embedding type from halfvec to bit and change the related topic query to leverage the new bit index by changing the search algorithm from inner product to Hamming distance. This will reduce our index sizes by 90%, severely reducing the impact of embeddings on our storage. By making the related query a bit smarter, we can have zero impact on recall by using the index to over-capture N*2 results, then re-ordering those N*2 using the full halfvec vectors and taking the top N. The expected impact is to go from 200ms to <20ms for cache misses and from a 2.5GB index to a 250MB index on a large site.

Another tool is migrating our index type from IVFFLAT to HNSW, which can increase the cache misses performance even further, eventually putting us in the under 5ms territory. 

Co-authored-by: Roman Rizzi <[email protected]>
2024-10-14 13:26:03 -03:00
Kelv 6615104389 DEV: Switch to use pnpm (#833) 2024-10-14 13:37:20 +02:00
Hoa Nguyen 94010a5f78 FEATURE: Tools for models from Ollama provider (#819)
Adds support for Ollama function calling
2024-10-11 07:25:53 +11:00
Sam 6c4c96e83c FEATURE: allow persona to only force tool calls on limited replies (#827)
This introduces another configuration that allows operators to
limit the amount of interactions with forced tool usage.

Forced tools are very handy in initial llm interactions, but as
conversation progresses they can hinder by slowing down stuff
and adding confusion.
2024-10-11 07:23:42 +11:00
Mark VanLandingham 52d90cf1bc DEV: Add apply_modifier for SemanticTopicQuery topics list (#830) 2024-10-10 12:13:16 -05:00
Bianca Nenciu c5b323fc07 DEV: Fix mismatched column types in tests (#826)
The primary key is usually a bigint column, but the foreign key columns
usually are of integer type. This can lead to issues when joining these
columns due to mismatched types and different value ranges.

In a recent core change, all bigint sequences will start at a very high
value in the test environment to surface this type of errors. The same
change also added a temporary API that changes the column type to bigint
in order to allow for the tests to run.

The plugin API is only temporary and it is important for these plugins
to migrate their columns to bigint to avoid issues in the future.
2024-10-10 18:39:36 +03:00
Rafael dos Santos Silva 95e70474fd DEV: Skip flaky test (#829) 2024-10-10 12:02:31 -03:00
Martin Brennan 4f6b36147b UX: Remove AdminPageSubheader style override (#828)
In this core commit https://github.com/discourse/discourse/pull/29149 we
are changing the subheader title to H2 and making the size smaller,
this style override is no longer needed.
2024-10-10 17:18:32 +10:00
Mark VanLandingham 51494db236 REVERT: "DEV: Convert related-topics to gjs (#822)" (#825)
This reverts commit a3c6938cb3.
2024-10-09 10:10:03 -05:00
Sam e1a0eb6131 FEATURE: support chain halting and upload creation support (#821)
This adds chain halting (ability to terminate llm chain in a tool)
and the ability to create uploads in a tool

Together this lets us integrate custom image generators into a
custom tool.
2024-10-09 08:17:45 +11:00
Discourse Translator Bot 3170e14acb Update translations (#823) 2024-10-08 20:21:52 +02:00
Jarek Radosz a3c6938cb3 DEV: Convert related-topics to gjs (#822) 2024-10-08 14:16:08 +02:00
Sam 545500b329 FEATURE: allows forced LLM tool use (#818)
* FEATURE: allows forced LLM tool use

Sometimes we need to force LLMs to use tools, for example in RAG
like use cases we may want to force an unconditional search.

The new framework allows you backend to force tool usage.

Front end commit to follow

* UI for forcing tools now works, but it does not react right

* fix bugs

* fix tests, this is now ready for review
2024-10-05 09:46:57 +10:00
Sam c294b6d394 FEATURE: allow llm triage to automatically hide posts (#820)
Previous to this change we could flag, but there was no way
to hide content and treat the flag as spam.

We had the option to hide topics, but this is not desirable for
a spam reply.

New option allows triage to hide a post if it is a reply, if the
post happens to be the first post on the topic, the topic will
be hidden.
2024-10-04 16:11:30 +10:00
110a1629aa DEV: Update rate limits for image captioning (#816)
This PR updates the rate limits for AI helper so that image caption follows a specific rate limit of 20 requests per minute. This should help when uploading multiple files that need to be captioned. This PR also updates the UI so that it shows toast message with the extracted error message instead of having a blocking `popupAjaxError` error dialog.
---------

Co-authored-by: Rafael dos Santos Silva <[email protected]>
Co-authored-by: Penar Musaraj <[email protected]>
2024-10-02 10:36:35 -07:00
Discourse Translator Bot 7ae6c17236 Update translations (#814) 2024-10-02 08:51:12 +02:00
Martin Brennan 7325fb21ab DEV: Use section landing components for LLMs templates (#817)
Relies on https://github.com/discourse/discourse/pull/28477,
uses AdminSectionLandingWrapper and AdminSectionLandingItem
for the section items on the LLM page which are used to create
a new LLM config from a template.
2024-10-02 15:31:48 +10:00
Kris 62ba2fa4d7 UX: update icon and text for copying message (#815) 2024-10-01 18:38:57 -04:00
Hoa Nguyen 2063b3854f FEATURE: Add Ollama provider (#812)
This allows our users to add the Ollama provider and use it to serve our AI bot (completion/dialect).

In this PR, we introduce:

    DiscourseAi::Completions::Dialects::Ollama which would help us translate by utilizing Completions::Endpoint::Ollama
    Correct extract_completion_from and partials_from in Endpoints::Ollama

Also

    Add tests for Endpoints::Ollama
    Introduce ollama_model fabricator
2024-10-01 10:45:03 +10:00
Discourse Translator Bot c7eaea48f5 Update translations (#811) 2024-09-30 17:31:26 +10:00
Sam 5cbc9190eb FEATURE: RAG search within tools (#802)
This allows custom tools access to uploads and sophisticated searches using embedding.

It introduces:

 - A shared front end for listing and uploading files (shared with personas)
 -  Backend implementation of index.search function within a custom tool.

Custom tools now may search through uploaded files

function invoke(params) {
   return index.search(params.query)
}

This means that RAG implementers now may preload tools with knowledge and have high fidelity over
the search.

The search function support

    specifying max results
    specifying a subset of files to search (from uploads)

Also

 - Improved documentation for tools (when creating a tool a preamble explains all the functionality)
  - uploads were a bit finicky, fixed an edge case where the UI would not show them as updated
2024-09-30 17:27:50 +10:00
KrisandSam Saffron 18ecc843e5 UX: move templates to main LLM config tab, restyle (#813)
Restructures LLM config page so it is far clearer. 

Also corrects bugs around adding LLMs and having LLMs not editable post addition 
---------

Co-authored-by: Sam Saffron <[email protected]>
2024-09-30 17:15:11 +10:00
Hoa Nguyen 1002dc877d DEV: remove ignore column syntax for the removed provider column in completion prompt model (#810) 2024-09-30 08:57:23 +10:00
chapoi 8cf1798afe UX: AI composer helper z-index issue (#809) 2024-09-23 17:01:04 -04:00
Keegan George 95f80325e5 DEV: Prevent close of summary from outside clicks (#808)
Often it is helpful to have the summary box open while composing a reply to the topic. However, the summary box currently gets closed each time you click outside the box. In this PR we add `closeOnClickOutside: false` attribute to the `DMenu` options for summary box to prevent that from occurring.
2024-09-18 10:36:42 -07:00
Keegan George e666266473 DEV: Make indicator wave a reusable component (#807)
Previously we had some hardcoded markup with scss making a loading indicator wave. This code was being duplicated and used in both semantic search and summarization. We want to add the indicator wave to the AI helper diff modal as well and have the text flashing instead of the loading spinner. To ensure we do not repeat ourselves, in this PR we turn the summary indicator wave into a reusable template only component called: `AiIndicatorWave`. We then apply the usage of that component to semantic search, summarization, and the composer helper modal.
2024-09-18 09:53:54 -07:00
chapoi 1e155942bb UX: take composer height into account when calculating the max-height for topic summary (#806)
* remove unused import

* UX: take composer height into account when calculating the max-height for the topic summary
2024-09-18 14:54:41 +10:00
Discourse Translator Bot 101f1e9512 Update translations (#799) 2024-09-18 09:53:01 +10:00
Keegan George 513510d6d0 FIX: AI Helper not visible on iPads (#805)
This commit fixes an issue where the composer AI helper was not visible on iPad in DiscourseHub. This was due to the z-index being different for `reply-control` when Discourse Hub inserts its `footer-nav`
2024-09-17 16:43:15 -07:00
Sam 4b21eb7974 FEATURE: basic support for GPT-o models (#804)
Caveats

- No streaming, by design
- No tool support (including no XML tools)
- No vision

Open AI will revamt the model and more of these features may
become available.

This solution is a bit hacky for now
2024-09-17 09:41:00 +10:00
Keegan George 493d65af1f FIX: Diff modal closing along with composer menu on mobile (#803)
The `DiffModal` is triggered after selecting an option in the composer helper menu. After selecting an option, we should close the composer helper menu and only show the diff modal. On mobile, there was an edge-case where `this.args.close()` for was causing the closing of both the `DiffModal` and the `AiComposerHelperMenu`. This PR resolves that by ensuring the menu is closed _first_ asynchronously, followed by opening the relevant modal.
2024-09-16 14:00:41 -07:00
Sam 03eccbe392 FEATURE: Make tool support polymorphic (#798)
Polymorphic RAG means that we will be able to access RAG fragments both from AiPersona and AiCustomTool

In turn this gives us support for richer RAG implementations.
2024-09-16 08:17:17 +10:00
Keegan George b16390ae2a UX: Improve toast message location (#800) 2024-09-14 09:19:13 +10:00
Keegan George 9374cd7ac1 FIX: Keyboard shortcut should be platform specific (#801) 2024-09-14 09:18:07 +10:00
Keegan George 9cd14b0003 DEV: Move composer AI helper to toolbar (#796)
Previously we had moved the AI helper from the options menu to a selection menu that appears when selecting text in the composer. This had the benefit of making the AI helper a more discoverable feature. Now that some time has passed and the AI helper is more recognized, we will be moving it back to the composer toolbar.

This is better because:
- It consistent with other behavior and ways of accessing tools in the composer
- It has an improved mobile experience
- It reduces unnecessary code and keeps things easier to migrate when we have composer V2.
- It allows for easily triggering AI helper for all content by clicking the button instead of having to select everything.
2024-09-13 11:59:30 -07:00
Sam 5b9add0ac8 FEATURE: add a SambaNova LLM provider (#797)
Note, at the moment the context window is quite small, it is
mainly useful as a helper backend or hyde generator
2024-09-12 11:28:08 +10:00
chapoi 22d1e71dc9 UX: AI post helper DMenu styling (#770) 2024-09-11 05:45:48 +02:00
Sam 36ce88f356 FIX: support case insensitive setting lookup (#795) 2024-09-10 15:21:03 +10:00
Sam a5b5c3bebe PERF: speed up spec (#794)
~500ms -> ~100ms

It is still not a super fast spec given search is not free, but
it is a bit faster and clearer
2024-09-04 16:14:32 +10:00
Sam cabecb801e FEATURE: disable rate limiting when skipping hyde (#793)
Embedding search is rate limited due to potentially expensive
hyde operation (which require LLM access).

Embedding generally is very cheap compared to it. (usually 100x cheaper)

This raises the limit to 100 per minute for embedding searches,
while keeping the old 4 per minute for HyDE powered search.
2024-09-04 15:51:01 +10:00
Roman Rizzi c4c9dc2034 FIX: Display cached summaries with our new streamer. (#792)
Make sure the summary box is in the DOM before attempting to
display a cached summary.:
2024-09-03 18:45:28 -03:00
Sam a48acc894a FEATURE: more accurate and faster titles (#791)
Previously we waited 1 minute before automatically titling PMs

The new change introduces adding a title immediately after the the
llm replies

Prompt was also modified to include the LLM reply in title suggestion.

This helps situation like:

user: tell me a joke
llm: a very funy joke about horses

Then the title would be "A Funny Horse Joke"

Specs already covered some auto title logic, amended to also
catch the new message bus message we have been sending.
2024-09-03 15:52:20 +10:00
Discourse Translator Bot b0ae2138af Update translations (#774) 2024-09-02 18:00:14 +02:00
dependabot[bot] b0d1eee0ce Build(deps): Bump micromatch from 4.0.5 to 4.0.8 (#790)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

---
updated-dependencies:
- dependency-name: micromatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-01 12:45:48 +02:00
dependabot[bot] 8ea26e8cfc Build(deps-dev): Bump rexml from 3.3.3 to 3.3.6 (#768)
Bumps [rexml](https://github.com/ruby/rexml) from 3.3.3 to 3.3.6.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.3.3...v3.3.6)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-01 12:23:09 +02:00
Roman Rizzi db5cbfb148 FIX: Bail earlier when a chat thread has no messages (#789) 2024-08-30 17:17:14 -03:00
Roman RizziandPenar Musaraj ed97827f49 FIX: Correctly display errors when parent module needs to be disabled first (#788)
* FIX: Correctly display errors when parent module needs to be disabled first

* Update spec/configuration/llm_validator_spec.rb

Co-authored-by: Penar Musaraj <[email protected]>

---------

Co-authored-by: Penar Musaraj <[email protected]>
2024-08-30 17:16:11 -03:00
Roman Rizzi e408cd080c FIX: coerce value before downcasing the hyde param (#787) 2024-08-30 12:13:29 -03:00
Kris 4a1874265e UX: replace "share" with "share-alt" icon (#784) 2024-08-30 12:13:09 -03:00
Sam 584753cf60 FIX: we were never reindexing old content (#786)
* FIX: we were never reindexing old content

Embedding backfill contains logic for searching for old content
change and then backfilling.

Unfortunately it was excluding all topics that had embedding
unconditionally, leading to no backfill ever happening.


This change adds a test and ensures we backfill.

* over select results, this ensures we will be more likely to find
ai results when filtered
2024-08-30 14:37:55 +10:00
Sam eee8e72756 FEATURE: API scope for semantic search (#785)
The new API scope allows restricting access to semantic search
only.
2024-08-30 09:35:20 +10:00
Keegan George 4176fbd3dd UX: Summary container should be fixed instead of growing (#783) 2024-08-29 16:32:05 -07:00
Keegan George fdadfa029e FEATURE: smooth streaming animation for summarization (#778) 2024-08-29 15:07:07 -07:00
Keegan George 94f6c632bf DEV: Publish AI Bot PM title update to message bus channel (#781) 2024-08-29 14:48:44 -07:00
Sam 41054c4fb8 FEATURE: improve site setting search (#780)
This improves the site setting search so it performs a somewhat
fuzzy match.

Previously it did not handle seperators such as "space" and a
term such as "min_post_length" would not find "min_first_post_length"

A more liberal search algorithm makes it easier to the AI to
navigate settings.

* Minor fix, {{and parameter.enum parameter.enum.length}} is non
obviously broken.


If parameter.enum is a tracked array it will return the object
cause embers and helper implementation.

This corrects an issue where enum keeps on selecting itself by
mistake.
2024-08-29 16:05:38 +10:00
Keegan George 943504049c FIX: Prevent proofreading when there is no content (#779) 2024-08-28 12:21:34 -07:00
Rafael dos Santos Silva a08d168740 FEATURE: Initial support for seeded LLMs (#756) 2024-08-28 15:57:58 -03:00
Sam 0687ec75c3 FEATURE: allow embedding based search without hyde (#777)
This allows callers of embedding based search to bypass hyde.

Hyde will expand the search term using an LLM, but if an LLM is
performing the search we can skip this expansion.

It also introduced some tests for the controller which we did not have
2024-08-28 14:17:34 +10:00
Roman Rizzi 72607c3560 Revert "FEATURE: Triage rule can skip posts created via email (#775)" (#776)
This reverts commit 6af415f7f0.
2024-08-27 15:45:55 -03:00
Roman Rizzi 6af415f7f0 FEATURE: Triage rule can skip posts created via email (#775) 2024-08-27 12:04:55 -03:00
Jarek Radosz 1b209f5fb1 DEV: Update ace-editor usage (#773)
AceEditor is now a glimmer component (see: https://github.com/discourse/discourse/pull/28492) and it follows the "data down, actions up" pattern.
2024-08-27 13:52:55 +02:00
Keegan Georgeandchapoi 50c2d56aff UX: Topic summary UI improvements due to DMenu changes (#772)
Co-authored-by: chapoi <[email protected]>
2024-08-26 08:32:39 -07:00
SamandMartin Brennan f148452f4c FEATURE: single click proofreading (#769)
Previously there was too much work proofreading text, new implementation
provides a single shortcut and easy way of proofreading text.


Co-authored-by: Martin Brennan <[email protected]>
2024-08-26 15:43:40 +10:00
Roman Rizzi c6aeabbfc0 FIX: Malformed message in systemless + inline img scenario (#771) 2024-08-23 16:41:57 -03:00
Roman Rizzi eac83eb619 FIX: Triage's search_for_text should be case-insensitive (#767) 2024-08-22 18:32:42 -03:00
Roman Rizzi 9019e90b87 FIX: urlEditable must be true for all providers except Bedrock (#766) 2024-08-22 11:31:28 -03:00
David Taylor 3c7bd9bbd3 FIX: Tool editing code editor resetting on every keypress (#765)
`withEventValue` is not needed here, because the `onChange` event comes from ace, not a normal DOM event. But even with that fix, it seems AceEditor doesn't yet work well with the DDAU pattern. On every keypress, the editor re-renders and puts the cursor back at the beginning.

For now, this commit removes the `@onChange` hook, so we go back to relying on the two-way binding of `@content`.

Followup to a5a39dd2ee
2024-08-22 10:18:16 +01:00
chapoi 324af23b72 UX: add dark-light mixin for better visibility in dark themes (#764) 2024-08-22 01:24:18 +02:00
Keegan George da9e192949 FIX: Mobile composer helper trigger should work on Android and iOS (#763) 2024-08-21 16:19:25 -07:00
chapoiandKeegan George 10dae65740 UX: style & position AI helper in composer (#758)
Co-authored-by: Keegan George <[email protected]>
2024-08-21 11:01:03 -07:00
Roman Rizzi 64641b6175 FEATURE: LLM Triage support for systemless models. (#757)
* FEATURE: LLM Triage support for systemless models.

This change adds support for OSS models without support for system messages. LlmTriage's system message field is no longer mandatory. We now send the post contents in a separate user message.

* Models using Ollama can also disable system prompts
2024-08-21 11:41:55 -03:00
Sam 97fc822cb6 FEATURE: Allow specific groups access to summary feature on PMs (#760)
New `ai_pm_summarization_allowed_groups` can be used to allow
visibility of the summarization feature on PMs.

This can be useful on forums where a lot of communication happens
inside PMs.
2024-08-21 07:58:24 +10:00
Keegan George c0328267be FIX: Add original upload markdown on caption failure (#762) 2024-08-20 12:53:51 -07:00
Discourse Translator Bot dd63ade348 Update translations (#761) 2024-08-20 10:58:36 -03:00
Keegan George 4a40531143 FIX: Clicking on summary links breaks summary (#759) 2024-08-20 10:57:23 -03:00
Keegan George bfe3b1c3b8 FIX: Modals in composer helper menu not working (#755) 2024-08-16 10:08:58 -07:00
Roman Rizzi f789d3ee96 FIX: Triage-flagged posts didn't have a score. (#752)
The score will contain the LLM result, and make sure the flag isn't displayed when a minimum score threshold is present.
2024-08-14 15:54:09 -03:00
Keegan George 867cd54556 DEV: Reduce megapixel requirement for auto image captioning (#754) 2024-08-14 11:26:56 -07:00
Keegan George 23b88537d9 FIX: Prevent AI caption setting from showing unless all criteria is met (#753) 2024-08-14 10:17:36 -07:00
Discourse Translator Bot 72980a3d84 Update translations (#751) 2024-08-13 16:33:25 +02:00
Sam 14443bf890 FIX: more robust summary implementation (#750)
When navigating between topic we were not correctly resetting
internal state for summarization. This leads to a situation where
incorrect summaries can be displayed to users and wrong summaries
can be displayed.

Additionally our controller for grabbing summaries was always
streaming results via message bus, which could be delayed when
sidekiq is overloaded. We now will return the cached summary
right away if it is available direct from REST endpoint.
2024-08-13 08:47:47 -03:00
Keegan George f72ab12761 DEV: Clearly separate post/composer helper settings (#747) 2024-08-12 15:40:23 -07:00
Keegan George a1eb1ba180 FIX: AI helper not being shown on mobile view (#749) 2024-08-12 15:32:49 -07:00
Sam cf1e15ef12 FIX: gemini 0801 tool calls (#748)
Gemini experimental model requires tool_config.

Previously defaults would apply.

This corrects prompts containing multiple tools on gemini.
2024-08-12 16:10:16 +10:00
Discourse Translator Bot a7c0ddba97 Update translations (#741) 2024-08-08 18:12:35 -03:00
Keegan George 0be292f247 FIX: auto_image_caption not always present for current user. (#746) 2024-08-08 13:37:26 -07:00
Keegan George 1d6a6c9f8f FEATURE: Stream other post helper options (#745) 2024-08-08 11:32:39 -07:00
Rafael dos Santos Silva 1686a8a683 DEV: Move to single table per embeddings type (#561)
Also move us to halfvecs for speed and disk usage gains
2024-08-08 11:55:20 -03:00
Roman Rizzi 20efc9285e FIX: Correctly save provider-specific params for new models. (#744)
Creating a new model, either manually or from presets, doesn't initialize the `provider_params` object, meaning their custom params won't persist.

Additionally, this change adds some validations for Bedrock params, which are mandatory, and a clear message when a completion fails because we cannot build the URL.
2024-08-07 16:08:56 -03:00
Rafael dos Santos Silva 3b5245dc54 FIX: Handle nil reply_to_post in AI Bot event handler (#743)
Somehow it's possible to have a nil `post.reply_to_post` with
a non-nil `post.reply_to_post_number`.
2024-08-07 14:22:32 -03:00
Keegan George 1254d7c7d0 REFACTOR: AI Composer Helper Menu (#715) 2024-08-06 10:57:39 -07:00
Roman Rizzi 7b4c099673 FIX: LlmModel validations. (#742)
- Validate fields to reduce the chance of breaking features by a misconfigured model.
- Fixed a bug where the URL might get deleted during an update.
- Display a warning when a model is currently in use.
2024-08-06 14:35:35 -03:00
chapoiandRafael Silva d17bbc2dc4 UX: summary fixed positioning (#740)
Co-authored-by: Rafael Silva <[email protected]>
2024-08-05 12:39:08 -03:00
dependabot[bot] 5c9f610fa4 Build(deps-dev): Bump rexml from 3.3.1 to 3.3.3 (#738)
Bumps [rexml](https://github.com/ruby/rexml) from 3.3.1 to 3.3.3.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.3.1...v3.3.3)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 13:10:20 +02:00
Keegan George 2ff3fe3a9f UX: Use stacked line chart for post sentiment (#737) 2024-08-02 14:23:29 -07:00
Roman Rizzi acc3f817e1 UX: Clarify purpose of OpenAI API key setting (#739) 2024-08-02 15:57:40 -03:00
Sam 948cf893a9 FIX: Add tool support to open ai compatible dialect and vllm (#734)
* FIX: Add tool support to open ai compatible dialect and vllm

Automatic tools are in progress in vllm see: https://github.com/vllm-project/vllm/pull/5649

Even when they are supported, initial support will be uneven, only some models have native tool support
notably mistral which has some special tokens for tool support.

After the above PR lands in vllm we will still need to swap to
XML based tools on models without native tool support.

* fix specs
2024-08-02 09:52:33 -03:00
Natalie Tay b7ac229547 DEV: Pin plugin for Discourse < 3.4.0.beta1-dev (#735) 2024-08-02 17:03:27 +08:00
Rafael dos Santos Silva 7dc0a2a20a DEV: Log error responses on Gemini / Cloudflare embeddings requests (#736) 2024-08-01 17:25:24 -03:00
Rafael dos Santos Silva 9d887ad4ac FIX: Use correct date for cached summary (#733) 2024-07-31 15:21:26 -03:00
Rafael dos Santos Silva d4c90bc2ba FEATURE: Make emotion analysis enabled by default (#732) 2024-07-31 12:00:00 -03:00
Sam c16c622b53 FIX: properly pass errors to client (#731)
render_json_error expects a AR model not a serializer, using a
serializer eats up the error message
2024-07-31 17:53:18 +10:00
Roman Rizzi bed044448c DEV: Remove old code now that features rely on LlmModels. (#729)
* DEV: Remove old code now that features rely on LlmModels.

* Hide old settings and migrate persona llm overrides

* Remove shadowing special URL + seeding code. Use srv:// prefix instead.
2024-07-30 13:44:57 -03:00
Discourse Translator Bot 73a2b15e91 Update translations (#730) 2024-07-30 11:08:03 -03:00
Natalie Tay 7cd7f71857 DEV: Promote historical post-deploy migrations (#728) 2024-07-30 01:44:57 +08:00
Rafael dos Santos Silva 665637fbad FIX: Properly fix ai_summaries table sequence (#727)
* FIX: Properly fix ai_summaries table sequence

Previous attempt at 3815360 could fail due to a race introduced in 1b0ba91 where summaries are migrated to core in a post_migrate erroneously.
2024-07-26 14:45:01 -03:00
Jan Cernik d58913b227 FIX: Collapse Summarize button on mobile (#726) 2024-07-25 16:10:53 -03:00
Jan Cernik d9dad56c6a UX: Use DMenu for topic summarization (#724) 2024-07-25 10:47:18 -03:00
Roman Rizzi 5c196bca89 FEATURE: Track if a model can do vision in the llm_models table (#725)
* FEATURE: Track if a model can do vision in the llm_models table

* Data migration
2024-07-24 16:29:47 -03:00
Discourse Translator Bot 06e239321b Update translations (#723) 2024-07-23 15:23:51 +02:00
Jan Cernik 738cd99ad6 FIX: Add bottom spacing for the new topic map (#722) 2024-07-22 22:21:23 -03:00
Rafael dos Santos Silva 3502f0f1cd FEATURE: GPT4o Tokenizer (#721) 2024-07-22 15:26:14 -03:00
Roman Rizzi 7f2c3a1ab9 DEV: Pin plugin for v3.2 to guarantee Ruby 3.3 compat (#719) 2024-07-17 13:16:14 -03:00
Joffrey JAFFEUX e4145ad6a6 FIX: ace-editor is now in discourse bundle, not admin (#717) 2024-07-17 11:58:50 +02:00
Discourse Translator Bot 4d8090002f Update translations (#716) 2024-07-16 22:14:09 +02:00
Roman Rizzi f328b81c78 FIX: Make sure custom tool enums follow json-schema. (#718)
Enums didn't work as expected because we the dialect couldn't translate
them correctly. It doesn't understand what "enum_values" is.
2024-07-16 14:23:17 -03:00
Roman Rizzi 0a8195242b FIX: Limit system message size to 60% of available tokens. (#714)
Using RAG fragments can lead to considerably big system messages, which becomes problematic when models have a smaller context window.

Before this change, we only look at the rest of the conversation to make sure we don't surpass the limit, which could lead to two unwanted scenarios when having large system messages:

All other messages are excluded due to size.
The system message already exceeds the limit.

As a result, I'm putting a hard-limit of 60% of available tokens. We don't want to aggresively truncate because if rag fragments are included, the system message contains a lot of context to improve the model response, but we also want to make room for the recent messages in the conversation.
2024-07-12 15:09:01 -03:00
Martin Brennan 5c1ab85583 DEV: More topic title prompt tweaks (#712)
Followup 8d4a67fbe2

The prompt worked better, but it took the instructions
about never using lowercase a little too literally,
it wasn't using it for things like LLM or Discourse,
also it was almost always framing the title as questions
so now I asked it for a mix of questions and statements
because that's less ambiguous.
2024-07-11 10:14:53 +10:00
Keegan George 08355ea5d8 FEATURE: Show post helper as bottom modal on mobile (#704) 2024-07-10 11:01:05 -07:00
Martin Brennan 8d4a67fbe2 DEV: Tweak topic title generator prompt (#710)
Changes the title generator prompt to avoid clickbait-y
titles and also try to avoid AI's favourite title format,
which is "Some Thing: Other Thing"

Leaving the chat thread title generator for now, that's
not as important, the bizarre titles add to the experience there.
2024-07-10 14:31:59 +10:00
453 changed files with 22050 additions and 11436 deletions
+5 -6
View File
@@ -1,8 +1,7 @@
< 3.4.0.beta3-dev: decf1bb49d737ea15308400f22f89d1d1e71d13d
< 3.4.0.beta1-dev: 9d887ad4ace8e33c3fe7dbb39237e882c08b4f0b
< 3.3.0.beta5-dev: 4d8090002f6dcd8e34d41033606bf131fa221475
< 3.3.0.beta2-dev: 61890b667c06299841ae88946f84a112f00060e1
< 3.3.0.beta1-dev: a65c4076a63104b61d213534db7f89aa3c8acd9e
< 3.2.0.beta5-dev: c70f43f130fb98e4a1adb1afd9b621aaa49088e3
< 3.2.0.beta4-dev: 493b48477a9288f2f1b6f021954088705a6673d6
< 3.2.0.beta2-dev: 3bced1c6f525553fe2b5b79d29bd66c1cf2a47d5
< 3.3.0.beta1-dev: d94234d89f50733678a5c62809445198a9fda74b
3.2.999: d94234d89f50733678a5c62809445198a9fda74b
3.1.999: c0415bb7eb878b1b7abf112d65cba981030df8df
< 3.2.0.beta1-dev: 3e7c99de894b03e9161d9481da118fe893991a1d
3.1.0.beta3: 057fbe1ce6
+1
View File
@@ -1,3 +1,4 @@
node_modules
/gems
/auto_generated
.env
+2
View File
@@ -0,0 +1,2 @@
engine-strict = true
auto-install-peers = false
+1 -3
View File
@@ -32,8 +32,7 @@ GEM
rack (3.1.6)
rainbow (3.1.1)
regexp_parser (2.9.2)
rexml (3.3.1)
strscan
rexml (3.3.9)
rubocop (1.64.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
@@ -70,7 +69,6 @@ GEM
rubocop (~> 1.61)
rubocop-rspec (~> 3, >= 3.0.1)
ruby-progressbar (1.13.0)
strscan (3.1.0)
syntax_tree (6.2.0)
prettier_print (>= 1.2.0)
tzinfo (2.0.6)
@@ -1,8 +1,13 @@
import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({
queryParams: {
llmTemplate: { refreshModel: true },
},
async model() {
const record = this.store.createRecord("ai-llm");
record.provider_params = {};
return record;
},
@@ -12,5 +17,9 @@ export default DiscourseRoute.extend({
"allLlms",
this.modelFor("adminPlugins.show.discourse-ai-llms")
);
controller.set(
"llmTemplate",
this.paramsFor(this.routeName).llmTemplate || null
);
},
});
@@ -4,7 +4,9 @@ export default DiscourseRoute.extend({
async model(params) {
const allLlms = this.modelFor("adminPlugins.show.discourse-ai-llms");
const id = parseInt(params.id, 10);
return allLlms.findBy("id", id);
const record = allLlms.findBy("id", id);
record.provider_params = record.provider_params || {};
return record;
},
setupController(controller, model) {
@@ -10,6 +10,8 @@ export default DiscourseRoute.extend({
record.set("rag_chunk_tokens", 374);
record.set("rag_chunk_overlap_tokens", 10);
record.set("rag_conversation_chunks", 10);
record.set("allow_personal_messages", true);
record.set("tool_details", false);
return record;
},
@@ -1 +1,5 @@
<AiLlmsListEditor @llms={{this.allLlms}} @currentLlm={{this.model}} />
<AiLlmsListEditor
@llms={{this.allLlms}}
@currentLlm={{this.model}}
@llmTemplate={{this.llmTemplate}}
/>
@@ -14,6 +14,9 @@ module DiscourseAi
llms,
each_serializer: LlmModelSerializer,
root: false,
scope: {
llm_usage: DiscourseAi::Configuration::LlmEnumerator.global_usage,
},
).as_json,
meta: {
provider_params: LlmModel.provider_params,
@@ -36,26 +39,34 @@ module DiscourseAi
llm_model = LlmModel.new(ai_llm_params)
if llm_model.save
llm_model.toggle_companion_user
render json: { ai_persona: LlmModelSerializer.new(llm_model) }, status: :created
render json: LlmModelSerializer.new(llm_model), status: :created
else
render_json_error LlmModelSerializer.new(llm_model)
render_json_error llm_model
end
end
def update
llm_model = LlmModel.find(params[:id])
if llm_model.seeded?
return render_json_error(I18n.t("discourse_ai.llm.cannot_edit_builtin"), status: 403)
end
if llm_model.update(ai_llm_params(updating: llm_model))
llm_model.toggle_companion_user
render json: LlmModelSerializer.new(llm_model)
else
render_json_error LlmModelSerializer.new(llm_model)
render_json_error llm_model
end
end
def destroy
llm_model = LlmModel.find(params[:id])
if llm_model.seeded?
return render_json_error(I18n.t("discourse_ai.llm.cannot_delete_builtin"), status: 403)
end
in_use_by = DiscourseAi::Configuration::LlmValidator.new.modules_using(llm_model)
if !in_use_by.empty?
@@ -97,6 +108,8 @@ module DiscourseAi
private
def ai_llm_params(updating: nil)
return {} if params[:ai_llm].blank?
permitted =
params.require(:ai_llm).permit(
:display_name,
@@ -106,20 +119,30 @@ module DiscourseAi
:max_prompt_tokens,
:api_key,
:enabled_chat_bot,
:vision_enabled,
)
provider = updating ? updating.provider : permitted[:provider]
permit_url =
(updating && updating.url != LlmModel::RESERVED_VLLM_SRV_URL) ||
provider != LlmModel::BEDROCK_PROVIDER_NAME
permit_url = provider != LlmModel::BEDROCK_PROVIDER_NAME
permitted[:url] = params.dig(:ai_llm, :url) if permit_url
new_url = params.dig(:ai_llm, :url)
permitted[:url] = new_url if permit_url && new_url
extra_field_names = LlmModel.provider_params.dig(provider&.to_sym, :fields).to_a
received_prov_params = params.dig(:ai_llm, :provider_params)
permitted[:provider_params] = received_prov_params.slice(
*extra_field_names,
).permit! if !extra_field_names.empty? && received_prov_params.present?
extra_field_names = LlmModel.provider_params.dig(provider&.to_sym)
if extra_field_names.present?
received_prov_params =
params.dig(:ai_llm, :provider_params)&.slice(*extra_field_names.keys)
if received_prov_params.present?
received_prov_params.each do |pname, value|
if extra_field_names[pname.to_sym] == :checkbox
received_prov_params[pname] = ActiveModel::Type::Boolean.new.cast(value)
end
end
permitted[:provider_params] = received_prov_params.permit!
end
end
permitted
end
@@ -5,8 +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 indexing_status_check]
before_action :find_ai_persona, only: %i[show update destroy create_user]
def index
ai_personas =
@@ -41,7 +40,7 @@ module DiscourseAi
def create
ai_persona = AiPersona.new(ai_persona_params.except(:rag_uploads))
if ai_persona.save
RagDocumentFragment.link_persona_and_uploads(ai_persona, attached_upload_ids)
RagDocumentFragment.link_target_and_uploads(ai_persona, attached_upload_ids)
render json: {
ai_persona: LocalizedAiPersonaSerializer.new(ai_persona, root: false),
@@ -59,11 +58,11 @@ module DiscourseAi
def update
if @ai_persona.update(ai_persona_params.except(:rag_uploads))
RagDocumentFragment.update_persona_uploads(@ai_persona, attached_upload_ids)
RagDocumentFragment.update_target_uploads(@ai_persona, attached_upload_ids)
render json: LocalizedAiPersonaSerializer.new(@ai_persona, root: false)
else
render_json_error LocalizedAiPersonaSerializer.new(@ai_persona, root: false)
render_json_error @ai_persona
end
end
@@ -71,43 +70,97 @@ module DiscourseAi
if @ai_persona.destroy
head :no_content
else
render_json_error LocalizedAiPersonaSerializer.new(@ai_persona, root: false)
render_json_error @ai_persona
end
end
def upload_file
file = params[:file] || params[:files].first
def stream_reply
persona =
AiPersona.find_by(name: params[:persona_name]) ||
AiPersona.find_by(id: params[:persona_id])
return render_json_error(I18n.t("discourse_ai.errors.persona_not_found")) if persona.nil?
if !SiteSetting.ai_embeddings_enabled?
raise Discourse::InvalidAccess.new("Embeddings not enabled")
return render_json_error(I18n.t("discourse_ai.errors.persona_disabled")) if !persona.enabled
if persona.default_llm.blank?
return render_json_error(I18n.t("discourse_ai.errors.no_default_llm"))
end
validate_extension!(file.original_filename)
validate_file_size!(file.tempfile.size)
if params[:query].blank?
return render_json_error(I18n.t("discourse_ai.errors.no_query_specified"))
end
hijack do
upload =
UploadCreator.new(
file.tempfile,
file.original_filename,
type: "discourse_ai_rag_upload",
skip_validations: true,
).create_for(current_user.id)
if !persona.user_id
return render_json_error(I18n.t("discourse_ai.errors.no_user_for_persona"))
end
if upload.persisted?
render json: UploadSerializer.new(upload)
else
render json: failed_json.merge(errors: upload.errors.full_messages), status: 422
if !params[:username] && !params[:user_unique_id]
return render_json_error(I18n.t("discourse_ai.errors.no_user_specified"))
end
user = nil
if params[:username]
user = User.find_by_username(params[:username])
return render_json_error(I18n.t("discourse_ai.errors.user_not_found")) if user.nil?
elsif params[:user_unique_id]
user = stage_user
end
raise Discourse::NotFound if user.nil?
topic_id = params[:topic_id].to_i
topic = nil
if topic_id > 0
topic = Topic.find(topic_id)
if topic.topic_allowed_users.where(user_id: user.id).empty?
return render_json_error(I18n.t("discourse_ai.errors.user_not_allowed"))
end
end
end
def indexing_status_check
render json: RagDocumentFragment.indexing_status(@ai_persona, @ai_persona.uploads)
hijack = request.env["rack.hijack"]
io = hijack.call
DiscourseAi::AiBot::ResponseHttpStreamer.queue_streamed_reply(
io: io,
persona: persona,
user: user,
topic: topic,
query: params[:query].to_s,
custom_instructions: params[:custom_instructions].to_s,
current_user: current_user,
)
end
private
AI_STREAM_CONVERSATION_UNIQUE_ID = "ai-stream-conversation-unique-id"
def stage_user
unique_id = params[:user_unique_id].to_s
field = UserCustomField.find_by(name: AI_STREAM_CONVERSATION_UNIQUE_ID, value: unique_id)
if field
field.user
else
preferred_username = params[:preferred_username]
username = UserNameSuggester.suggest(preferred_username || unique_id)
user =
User.new(
username: username,
email: "#{SecureRandom.hex}@invalid.com",
staged: true,
active: false,
)
user.custom_fields[AI_STREAM_CONVERSATION_UNIQUE_ID] = unique_id
user.save!
user
end
end
def find_ai_persona
@ai_persona = AiPersona.find(params[:id])
end
@@ -128,7 +181,6 @@ module DiscourseAi
:temperature,
:default_llm,
:user_id,
:mentionable,
:max_context_posts,
:vision_enabled,
:vision_max_pixels,
@@ -136,8 +188,13 @@ module DiscourseAi
:rag_chunk_overlap_tokens,
:rag_conversation_chunks,
:question_consolidator_llm,
:allow_chat,
:allow_chat_channel_mentions,
:allow_chat_direct_messages,
:allow_topic_mentions,
:allow_personal_messages,
:tool_details,
:forced_tool_count,
:force_default_llm,
allowed_group_ids: [],
rag_uploads: [:id],
)
@@ -152,40 +209,12 @@ module DiscourseAi
def permit_tools(tools)
return [] if !tools.is_a?(Array)
tools.filter_map do |tool, options|
tools.filter_map do |tool, options, force_tool|
break nil if !tool.is_a?(String)
options&.permit! if options && options.is_a?(ActionController::Parameters)
if options
[tool, options]
else
tool
end
end
end
def validate_extension!(filename)
extension = File.extname(filename)[1..-1] || ""
authorized_extensions = %w[txt md]
if !authorized_extensions.include?(extension)
raise Discourse::InvalidParameters.new(
I18n.t(
"upload.unauthorized",
authorized_extensions: authorized_extensions.join(" "),
),
)
end
end
def validate_file_size!(filesize)
max_size_bytes = 20.megabytes
if filesize > max_size_bytes
raise Discourse::InvalidParameters.new(
I18n.t(
"upload.attachments.too_large_humanized",
max_size: ActiveSupport::NumberHelper.number_to_human_size(max_size_bytes),
),
)
# this is simpler from a storage perspective, 1 way to store tools
[tool, options, !!force_tool]
end
end
end
@@ -5,7 +5,7 @@ module DiscourseAi
class AiToolsController < ::Admin::AdminController
requires_plugin ::DiscourseAi::PLUGIN_NAME
before_action :find_ai_tool, only: %i[show update destroy]
before_action :find_ai_tool, only: %i[test show update destroy]
def index
ai_tools = AiTool.all
@@ -21,6 +21,7 @@ module DiscourseAi
ai_tool.created_by_id = current_user.id
if ai_tool.save
RagDocumentFragment.link_target_and_uploads(ai_tool, attached_upload_ids)
render_serialized(ai_tool, AiCustomToolSerializer, status: :created)
else
render_json_error ai_tool
@@ -29,6 +30,7 @@ module DiscourseAi
def update
if @ai_tool.update(ai_tool_params)
RagDocumentFragment.update_target_uploads(@ai_tool, attached_upload_ids)
render_serialized(@ai_tool, AiCustomToolSerializer)
else
render_json_error @ai_tool
@@ -44,18 +46,13 @@ module DiscourseAi
end
def test
if params[:id].present?
ai_tool = AiTool.find(params[:id])
else
ai_tool = AiTool.new(ai_tool_params)
end
@ai_tool.assign_attributes(ai_tool_params) if params[:ai_tool]
parameters = params[:parameters].to_unsafe_h
# 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, context: {})
result = runner.invoke
if result.is_a?(Hash) && result[:error]
@@ -71,18 +68,28 @@ module DiscourseAi
private
def attached_upload_ids
params[:ai_tool][:rag_uploads].to_a.map { |h| h[:id] }
end
def find_ai_tool
@ai_tool = AiTool.find(params[:id])
@ai_tool = AiTool.find(params[:id].to_i)
end
def ai_tool_params
params.require(:ai_tool).permit(
:name,
:description,
:script,
:summary,
parameters: [:name, :type, :description, :required, :enum, enum_values: []],
)
params
.require(:ai_tool)
.permit(
:name,
:description,
:script,
:summary,
:rag_chunk_tokens,
:rag_chunk_overlap_tokens,
rag_uploads: [:id],
parameters: [:name, :type, :description, :required, enum: []],
)
.except(:rag_uploads)
end
end
end
@@ -0,0 +1,75 @@
# frozen_string_literal: true
module DiscourseAi
module Admin
class RagDocumentFragmentsController < ::Admin::AdminController
requires_plugin ::DiscourseAi::PLUGIN_NAME
def indexing_status_check
if params[:target_type] == "AiPersona"
@target = AiPersona.find(params[:target_id])
elsif params[:target_type] == "AiTool"
@target = AiTool.find(params[:target_id])
else
raise Discourse::InvalidParameters.new("Invalid target type")
end
render json: RagDocumentFragment.indexing_status(@target, @target.uploads)
end
def upload_file
file = params[:file] || params[:files].first
if !SiteSetting.ai_embeddings_enabled?
raise Discourse::InvalidAccess.new("Embeddings not enabled")
end
validate_extension!(file.original_filename)
validate_file_size!(file.tempfile.size)
hijack do
upload =
UploadCreator.new(
file.tempfile,
file.original_filename,
type: "discourse_ai_rag_upload",
skip_validations: true,
).create_for(current_user.id)
if upload.persisted?
render json: UploadSerializer.new(upload)
else
render json: failed_json.merge(errors: upload.errors.full_messages), status: 422
end
end
end
private
def validate_extension!(filename)
extension = File.extname(filename)[1..-1] || ""
authorized_extensions = %w[txt md]
if !authorized_extensions.include?(extension)
raise Discourse::InvalidParameters.new(
I18n.t(
"upload.unauthorized",
authorized_extensions: authorized_extensions.join(" "),
),
)
end
end
def validate_file_size!(filesize)
max_size_bytes = 20.megabytes
if filesize > max_size_bytes
raise Discourse::InvalidParameters.new(
I18n.t(
"upload.attachments.too_large_humanized",
max_size: ActiveSupport::NumberHelper.number_to_human_size(max_size_bytes),
),
)
end
end
end
end
end
@@ -6,6 +6,14 @@ module DiscourseAi
requires_plugin ::DiscourseAi::PLUGIN_NAME
requires_login
def show_debug_info_by_id
log = AiApiAuditLog.find(params[:id])
raise Discourse::NotFound if !log.topic
guardian.ensure_can_debug_ai_bot_conversation!(log.topic)
render json: AiApiAuditLogSerializer.new(log, root: false), status: 200
end
def show_debug_info
post = Post.find(params[:post_id])
guardian.ensure_can_debug_ai_bot_conversation!(post)
@@ -6,10 +6,21 @@ module DiscourseAi
requires_plugin ::DiscourseAi::PLUGIN_NAME
requires_login
before_action :ensure_can_request_suggestions
before_action :rate_limiter_performed!, except: %i[prompts]
before_action :rate_limiter_performed!
include SecureUploadEndpointHelpers
RATE_LIMITS = {
"default" => {
amount: 6,
interval: 3.minutes,
},
"caption_image" => {
amount: 20,
interval: 1.minute,
},
}.freeze
def suggest
input = get_text_param!
force_default_locale = params[:force_default_locale] || false
@@ -89,18 +100,26 @@ module DiscourseAi
end
end
def explain
def stream_suggestion
post_id = get_post_param!
term_to_explain = get_text_param!
text = get_text_param!
post = Post.includes(:topic).find_by(id: post_id)
prompt = CompletionPrompt.find_by(id: params[:mode])
raise Discourse::InvalidParameters.new(:mode) if !prompt || !prompt.enabled?
raise Discourse::InvalidParameters.new(:post_id) unless 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,
term_to_explain: term_to_explain,
text: text,
prompt: prompt.name,
custom_prompt: params[:custom_prompt],
)
render json: { success: true }, status: 200
@@ -153,13 +172,23 @@ module DiscourseAi
end
def rate_limiter_performed!
RateLimiter.new(current_user, "ai_assistant", 6, 3.minutes).performed!
action_rate_limit = RATE_LIMITS[action_name] || RATE_LIMITS["default"]
RateLimiter.new(
current_user,
"ai_assistant",
action_rate_limit[:amount],
action_rate_limit[:interval],
).performed!
end
def ensure_can_request_suggestions
if !current_user.in_any_groups?(SiteSetting.ai_helper_allowed_groups_map)
raise Discourse::InvalidAccess
end
allowed_groups =
(
SiteSetting.composer_ai_helper_allowed_groups_map |
SiteSetting.post_ai_helper_allowed_groups_map
)
raise Discourse::InvalidAccess if !current_user.in_any_groups?(allowed_groups)
end
end
end
@@ -0,0 +1,50 @@
# frozen_string_literal: true
module DiscourseAi
module Discord
class BotController < ::ApplicationController
requires_plugin ::DiscourseAi::PLUGIN_NAME
skip_before_action :verify_authenticity_token
def interactions
# Request signature verification
begin
verify_request!
rescue Ed25519::VerifyError
return head :unauthorized
end
body = request.body.read
interaction = JSON.parse(body, object_class: OpenStruct)
if interaction.type == 1
# Respond to Discord PING request
render json: { type: 1 }
else
if !SiteSetting.ai_discord_allowed_guilds_map.include?(interaction.guild_id)
return head :forbidden
end
response = { type: 5, data: { content: "Searching..." } }
hijack { render json: response }
# Respond to Discord command
Jobs.enqueue(:stream_discord_reply, interaction: body)
end
end
private
def verify_request!
signature = request.headers["X-Signature-Ed25519"]
timestamp = request.headers["X-Signature-Timestamp"]
verify_key.verify([signature].pack("H*"), "#{timestamp}#{request.raw_post}")
end
def verify_key
Ed25519::VerifyKey.new([SiteSetting.ai_discord_app_public_key].pack("H*")).freeze
end
end
end
end
@@ -7,8 +7,12 @@ module DiscourseAi
SEMANTIC_SEARCH_TYPE = "semantic_search"
MAX_HYDE_SEARCHES_PER_MINUTE = 4
MAX_SEARCHES_PER_MINUTE = 100
def search
query = params[:q].to_s
skip_hyde = params[:hyde].to_s.downcase == "false" || params[:hyde].to_s == "0"
if query.length < SiteSetting.min_search_term_length
raise Discourse::InvalidParameters.new(:q)
@@ -25,13 +29,25 @@ module DiscourseAi
semantic_search = DiscourseAi::Embeddings::SemanticSearch.new(guardian)
if !semantic_search.cached_query?(query)
RateLimiter.new(current_user, "semantic-search", 4, 1.minutes).performed!
if !skip_hyde && !semantic_search.cached_query?(query)
RateLimiter.new(
current_user,
"semantic-search",
MAX_HYDE_SEARCHES_PER_MINUTE,
1.minutes,
).performed!
else
RateLimiter.new(
current_user,
"semantic-search-non-hyde",
MAX_SEARCHES_PER_MINUTE,
1.minutes,
).performed!
end
hijack do
semantic_search
.search_for_topics(query)
.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)
@@ -39,6 +55,9 @@ module DiscourseAi
end
def quick_search
# this search function searches posts (vs: topics)
# it requires post embeddings and a reranker
# it will not perform a hyde expantion
query = params[:q].to_s
if query.length < SiteSetting.min_search_term_length
@@ -15,29 +15,21 @@ module DiscourseAi
channel = ::Chat::Channel.find(params[:channel_id])
guardian.ensure_can_join_chat_channel!(channel)
strategy = DiscourseAi::Summarization.default_strategy
raise Discourse::NotFound.new unless strategy
summarizer = DiscourseAi::Summarization.chat_channel_summary(channel, since)
raise Discourse::NotFound.new unless summarizer
guardian.ensure_can_request_summary!
RateLimiter.new(current_user, "channel_summary", 6, 5.minutes).performed!
hijack do
content = { content_title: channel.name }
content[:contents] = channel
.chat_messages
.where("chat_messages.created_at > ?", since.hours.ago)
.includes(:user)
.order(created_at: :asc)
.pluck(:id, :username_lower, :message)
.map { { id: _1, poster: _2, text: _3 } }
strategy = DiscourseAi::Summarization::Strategies::ChatMessages.new(channel, since)
summarized_text =
if content[:contents].empty?
if strategy.targets_data.empty?
I18n.t("discourse_ai.summarization.chat.no_targets")
else
strategy.summarize(content, current_user).dig(:summary)
summarizer.summarize(current_user)&.summarized_text
end
render json: { summary: summarized_text }
@@ -14,19 +14,29 @@ module DiscourseAi
RateLimiter.new(current_user, "summary", 6, 5.minutes).performed! if current_user
opts = params.permit(:skip_age_check)
skip_age_check = opts[:skip_age_check] == "true"
summarization_service = DiscourseAi::TopicSummarization.for(topic, current_user)
if params[:stream] && current_user
cached_summary = summarization_service.cached_summary
if cached_summary && !skip_age_check
render_serialized(cached_summary, AiTopicSummarySerializer)
return
end
Jobs.enqueue(
:stream_topic_ai_summary,
topic_id: topic.id,
user_id: current_user.id,
opts: opts.as_json,
skip_age_check: skip_age_check,
)
render json: success_json
else
hijack do
summary = DiscourseAi::TopicSummarization.summarize(topic, current_user, opts)
summary = summarization_service.summarize(skip_age_check: skip_age_check)
render_serialized(summary, AiTopicSummarySerializer)
end
end
+16 -7
View File
@@ -9,17 +9,24 @@ module ::Jobs
# TODO(roman): Add a way to automatically recover from errors, resulting in unindexed uploads.
def execute(args)
return if (upload = Upload.find_by(id: args[:upload_id])).nil?
return if (ai_persona = AiPersona.find_by(id: args[:ai_persona_id])).nil?
target_type = args[:target_type]
target_id = args[:target_id]
return if !target_type || !target_id
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)
tokenizer = vector_rep.tokenizer
chunk_tokens = ai_persona.rag_chunk_tokens
overlap_tokens = ai_persona.rag_chunk_overlap_tokens
chunk_tokens = target.rag_chunk_tokens
overlap_tokens = target.rag_chunk_overlap_tokens
fragment_ids = RagDocumentFragment.where(ai_persona: ai_persona, upload: upload).pluck(:id)
fragment_ids = RagDocumentFragment.where(target: target, upload: upload).pluck(:id)
# Check if this is the first time we process this upload.
if fragment_ids.empty?
@@ -39,7 +46,7 @@ module ::Jobs
overlap_tokens: overlap_tokens,
) do |chunk, metadata|
fragment_ids << RagDocumentFragment.create!(
ai_persona: ai_persona,
target: target,
fragment: chunk,
fragment_number: idx + 1,
upload: upload,
@@ -119,7 +126,9 @@ module ::Jobs
while overlap_token_ids.present?
begin
overlap = tokenizer.decode(overlap_token_ids) + split_char
padding = split_char
padding = " " if padding.empty?
overlap = tokenizer.decode(overlap_token_ids) + padding
break if overlap.encoding == Encoding::UTF_8
rescue StandardError
# it is possible that we truncated mid char
@@ -128,7 +137,7 @@ module ::Jobs
end
# remove first word it is probably truncated
overlap = overlap.split(" ", 2).last
overlap = overlap.split(/\s/, 2).last.to_s.lstrip
end
end
+2 -2
View File
@@ -17,10 +17,10 @@ module ::Jobs
fragments.map { |fragment| vector_rep.generate_representation_from(fragment) }
last_fragment = fragments.last
ai_persona = last_fragment.ai_persona
target = last_fragment.target
upload = last_fragment.upload
indexing_status = RagDocumentFragment.indexing_status(ai_persona, [upload])[upload.id]
indexing_status = RagDocumentFragment.indexing_status(target, [upload])[upload.id]
RagDocumentFragment.publish_status(upload, indexing_status)
end
end
+22
View File
@@ -0,0 +1,22 @@
# 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
+17
View File
@@ -0,0 +1,17 @@
# frozen_string_literal: true
module Jobs
class StreamDiscordReply < ::Jobs::Base
sidekiq_options retry: false
def execute(args)
interaction = args[:interaction]
if SiteSetting.ai_discord_search_mode == "persona"
DiscourseAi::Discord::Bot::PersonaReplier.new(interaction).handle_interaction!
else
DiscourseAi::Discord::Bot::Search.new(interaction).handle_interaction!
end
end
end
end
+13 -5
View File
@@ -7,27 +7,35 @@ module Jobs
def execute(args)
return unless post = Post.includes(:topic).find_by(id: args[:post_id])
return unless user = User.find_by(id: args[:user_id])
return unless args[:term_to_explain]
return unless args[:text]
topic = post.topic
reply_to = post.reply_to_post
return unless user.guardian.can_see?(post)
prompt = CompletionPrompt.enabled_by_name("explain")
prompt = CompletionPrompt.enabled_by_name(args[:prompt])
input = <<~TEXT
<term>#{args[:term_to_explain]}</term>
if prompt.id == CompletionPrompt::CUSTOM_PROMPT
prompt.custom_instruction = args[:custom_prompt]
end
if prompt.name == "explain"
input = <<~TEXT
<term>#{args[:text]}</term>
<context>#{post.raw}</context>
<topic>#{topic.title}</topic>
#{reply_to ? "<replyTo>#{reply_to.raw}</replyTo>" : nil}
TEXT
else
input = args[:text]
end
DiscourseAi::AiHelper::Assistant.new.stream_prompt(
prompt,
input,
user,
"/discourse-ai/ai-helper/explain/#{post.id}",
"/discourse-ai/ai-helper/stream_suggestion/#{post.id}",
)
end
end
+5 -5
View File
@@ -8,25 +8,25 @@ module Jobs
return unless topic = Topic.find_by(id: args[:topic_id])
return unless user = User.find_by(id: args[:user_id])
strategy = DiscourseAi::Summarization.default_strategy
strategy = DiscourseAi::Summarization.topic_summary(topic)
return if strategy.nil? || !Guardian.new(user).can_see_summary?(topic)
guardian = Guardian.new(user)
return unless guardian.can_see?(topic)
opts = args[:opts] || {}
skip_age_check = !!args[:skip_age_check]
streamed_summary = +""
start = Time.now
summary =
DiscourseAi::TopicSummarization
.new(strategy)
.summarize(topic, user, opts) do |partial_summary|
.new(strategy, user)
.summarize(skip_age_check: skip_age_check) do |partial_summary|
streamed_summary << partial_summary
# Throttle updates.
if (Time.now - start > 0.5) || Rails.env.test?
if (Time.now - start > 0.3) || Rails.env.test?
payload = { done: false, ai_topic_summary: { summarized_text: streamed_summary } }
publish_update(topic, user, payload)
@@ -1,17 +0,0 @@
# frozen_string_literal: true
module ::Jobs
class UpdateAiBotPmTitle < ::Jobs::Base
sidekiq_options retry: false
def execute(args)
return unless bot_user = User.find_by(id: args[:bot_user_id])
return unless bot = DiscourseAi::AiBot::Bot.as(bot_user, model: args[:model])
return unless post = Post.includes(:topic).find_by(id: args[:post_id])
return unless post.topic.custom_fields[DiscourseAi::AiBot::EntryPoint::REQUIRE_TITLE_UPDATE]
DiscourseAi::AiBot::Playground.new(bot).title_playground(post)
end
end
end
+25
View File
@@ -0,0 +1,25 @@
# frozen_string_literal: true
module ::Jobs
class UpdateHotTopicGist < ::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?
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
summarizer.force_summarize(Discourse.system_user)
end
end
end
+10 -8
View File
@@ -35,8 +35,6 @@ module Jobs
rebaked += populate_topic_embeddings(vector_rep, topics)
vector_rep.consider_indexing
return if rebaked >= limit
# Then, we'll try to backfill embeddings for topics that have outdated
@@ -54,9 +52,12 @@ module Jobs
# Finally, we'll try to backfill embeddings for topics that have outdated
# embeddings due to edits or new replies. Here we only do 10% of the limit
relation =
topics.where("#{table_name}.updated_at < ?", 7.days.ago).limit((limit - rebaked) / 10)
topics
.where("#{table_name}.updated_at < ?", 6.hours.ago)
.where("#{table_name}.updated_at < topics.updated_at")
.limit((limit - rebaked) / 10)
populate_topic_embeddings(vector_rep, relation)
populate_topic_embeddings(vector_rep, relation, force: true)
return if rebaked >= limit
@@ -79,8 +80,6 @@ module Jobs
rebaked += 1
end
vector_rep.consider_indexing
return if rebaked >= limit
# Then, we'll try to backfill embeddings for posts that have outdated
@@ -115,9 +114,12 @@ module Jobs
private
def populate_topic_embeddings(vector_rep, topics)
def populate_topic_embeddings(vector_rep, topics, force: false)
done = 0
ids = topics.where("#{vector_rep.topic_table_name}.topic_id IS NULL").pluck("topics.id")
topics = topics.where("#{vector_rep.topic_table_name}.topic_id IS NULL") if !force
ids = topics.pluck("topics.id")
ids.each do |id|
topic = Topic.find_by(id: id)
+57
View File
@@ -0,0 +1,57 @@
# frozen_string_literal: true
module ::Jobs
class SummariesBackfill < ::Jobs::Scheduled
every 5.minutes
cluster_concurrency 1
def execute(_args)
return if !SiteSetting.discourse_ai_enabled
return if !SiteSetting.ai_summarization_enabled
return if SiteSetting.ai_summary_backfill_maximum_topics_per_hour.zero?
system_user = Discourse.system_user
complete_t = AiSummary.summary_types[:complete]
backfill_candidates(complete_t)
.limit(current_budget(complete_t))
.each do |topic|
DiscourseAi::Summarization.topic_summary(topic).force_summarize(system_user)
end
gist_t = AiSummary.summary_types[:gist]
backfill_candidates(gist_t)
.limit(current_budget(gist_t))
.each { |topic| DiscourseAi::Summarization.topic_gist(topic).force_summarize(system_user) }
end
def backfill_candidates(summary_type)
Topic
.where("topics.word_count >= ?", SiteSetting.ai_summary_backfill_minimum_word_count)
.joins(<<~SQL)
LEFT OUTER JOIN ai_summaries ais ON
topics.id = ais.target_id AND
ais.target_type = 'Topic' AND
ais.summary_type = '#{summary_type}'
SQL
.where(
"ais.id IS NULL OR UPPER(ais.content_range) < topics.highest_post_number + 1",
) # (1..1) gets stored ad (1..2).
.order("ais.created_at DESC NULLS FIRST, topics.last_posted_at DESC")
end
def current_budget(type)
# Split budget in 12 intervals, but make sure is at least one.
base_budget = SiteSetting.ai_summary_backfill_maximum_topics_per_hour
limit_per_job = [base_budget, 12].max / 12
used_budget =
AiSummary.system.where("created_at > ?", 1.hour.ago).where(summary_type: type).count
current_budget = [(base_budget - used_budget), limit_per_job].min
return 0 if current_budget < 0
current_budget
end
end
end
+10 -1
View File
@@ -12,6 +12,15 @@ class AiApiAuditLog < ActiveRecord::Base
Vllm = 5
Cohere = 6
Ollama = 7
SambaNova = 8
end
def next_log_id
self.class.where("id > ?", id).where(topic_id: topic_id).order(id: :asc).pluck(:id).first
end
def prev_log_id
self.class.where("id < ?", id).where(topic_id: topic_id).order(id: :desc).pluck(:id).first
end
end
@@ -32,4 +41,4 @@ end
# post_id :integer
# feature_name :string(255)
# language_model :string(255)
#
# feature_context :jsonb
+91 -67
View File
@@ -1,8 +1,8 @@
# frozen_string_literal: true
class AiPersona < ActiveRecord::Base
# TODO remove this line 01-11-2024
self.ignored_columns = [:commands]
# TODO remove this line 01-1-2025
self.ignored_columns = %i[commands allow_chat mentionable]
# places a hard limit, so per site we cache a maximum of 500 classes
MAX_PERSONAS_PER_SITE = 500
@@ -20,6 +20,8 @@ class AiPersona < ActiveRecord::Base
validates :rag_chunk_tokens, numericality: { greater_than: 0, maximum: 50_000 }
validates :rag_chunk_overlap_tokens, numericality: { greater_than: -1, maximum: 200 }
validates :rag_conversation_chunks, numericality: { greater_than: 0, maximum: 1000 }
validates :forced_tool_count, numericality: { greater_than: -2, maximum: 100_000 }
has_many :rag_document_fragments, dependent: :destroy, as: :target
belongs_to :created_by, class_name: "User"
belongs_to :user
@@ -27,12 +29,7 @@ class AiPersona < ActiveRecord::Base
has_many :upload_references, as: :target, dependent: :destroy
has_many :uploads, through: :upload_references
has_many :rag_document_fragment, dependent: :destroy
has_many :rag_document_fragments, through: :ai_persona_rag_document_fragments
before_destroy :ensure_not_system
before_update :regenerate_rag_fragments
def self.persona_cache
@@ -55,30 +52,47 @@ class AiPersona < ActiveRecord::Base
persona_cache[:persona_users] ||= AiPersona
.where(enabled: true)
.joins(:user)
.pluck(
"ai_personas.id, users.id, users.username_lower, allowed_group_ids, default_llm, mentionable, allow_chat",
)
.map do |id, user_id, username, allowed_group_ids, default_llm, mentionable, allow_chat|
.map do |persona|
{
id: id,
user_id: user_id,
username: username,
allowed_group_ids: allowed_group_ids,
default_llm: default_llm,
mentionable: mentionable,
allow_chat: allow_chat,
id: persona.id,
user_id: persona.user_id,
username: persona.user.username_lower,
allowed_group_ids: persona.allowed_group_ids,
default_llm: persona.default_llm,
force_default_llm: persona.force_default_llm,
allow_chat_channel_mentions: persona.allow_chat_channel_mentions,
allow_chat_direct_messages: persona.allow_chat_direct_messages,
allow_topic_mentions: persona.allow_topic_mentions,
allow_personal_messages: persona.allow_personal_messages,
}
end
if user
persona_users.select { |mentionable| user.in_any_groups?(mentionable[:allowed_group_ids]) }
persona_users.select { |persona_user| user.in_any_groups?(persona_user[:allowed_group_ids]) }
else
persona_users
end
end
def self.allowed_chat(user: nil)
personas = persona_cache[:allowed_chat] ||= persona_users.select { |u| u[:allow_chat] }
def self.allowed_modalities(
user: nil,
allow_chat_channel_mentions: false,
allow_chat_direct_messages: false,
allow_topic_mentions: false,
allow_personal_messages: false
)
index =
"modality-#{allow_chat_channel_mentions}-#{allow_chat_direct_messages}-#{allow_topic_mentions}-#{allow_personal_messages}"
personas =
persona_cache[index.to_sym] ||= persona_users.select do |persona|
next true if allow_chat_channel_mentions && persona[:allow_chat_channel_mentions]
next true if allow_chat_direct_messages && persona[:allow_chat_direct_messages]
next true if allow_topic_mentions && persona[:allow_topic_mentions]
next true if allow_personal_messages && persona[:allow_personal_messages]
false
end
if user
personas.select { |u| user.in_any_groups?(u[:allowed_group_ids]) }
else
@@ -86,18 +100,6 @@ class AiPersona < ActiveRecord::Base
end
end
def self.mentionables(user: nil)
all_mentionables =
persona_cache[:mentionables] ||= persona_users.select do |mentionable|
mentionable[:mentionable]
end
if user
all_mentionables.select { |mentionable| user.in_any_groups?(mentionable[:allowed_group_ids]) }
else
all_mentionables
end
end
after_commit :bump_cache
def bump_cache
@@ -116,7 +118,11 @@ class AiPersona < ActiveRecord::Base
vision_max_pixels
rag_conversation_chunks
question_consolidator_llm
allow_chat
allow_chat_channel_mentions
allow_chat_direct_messages
allow_topic_mentions
allow_personal_messages
force_default_llm
name
description
allowed_group_ids
@@ -131,6 +137,8 @@ class AiPersona < ActiveRecord::Base
instance_attributes[attr] = value
end
instance_attributes[:username] = user&.username_lower
if persona_class
instance_attributes.each do |key, value|
# description/name are localized
@@ -140,17 +148,23 @@ class AiPersona < ActiveRecord::Base
end
options = {}
force_tool_use = []
tools =
self.tools.filter_map do |element|
klass = nil
if element.is_a?(String) && element.start_with?("custom-")
custom_tool_id = element.split("-", 2).last.to_i
element = [element] if element.is_a?(String)
inner_name, current_options, should_force_tool_use =
element.is_a?(Array) ? element : [element, nil]
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)
end
else
inner_name, current_options = element.is_a?(Array) ? element : [element, nil]
inner_name = inner_name.gsub("Tool", "")
inner_name = "List#{inner_name}" if %w[Categories Tags].include?(inner_name)
@@ -159,9 +173,10 @@ class AiPersona < ActiveRecord::Base
options[klass] = current_options if current_options
rescue StandardError
end
klass
end
force_tool_use << klass if should_force_tool_use
klass
end
ai_persona_id = self.id
@@ -181,6 +196,8 @@ class AiPersona < ActiveRecord::Base
end
define_method(:tools) { tools }
define_method(:force_tool_use) { force_tool_use }
define_method(:forced_tool_count) { @ai_persona&.forced_tool_count }
define_method(:options) { options }
define_method(:temperature) { @ai_persona&.temperature }
define_method(:top_p) { @ai_persona&.top_p }
@@ -230,14 +247,17 @@ class AiPersona < ActiveRecord::Base
def regenerate_rag_fragments
if rag_chunk_tokens_changed? || rag_chunk_overlap_tokens_changed?
RagDocumentFragment.where(ai_persona: self).delete_all
RagDocumentFragment.where(target: self).delete_all
end
end
private
def chat_preconditions
if allow_chat && !default_llm
if (
allow_chat_channel_mentions || allow_chat_direct_messages || allow_topic_mentions ||
force_default_llm
) && !default_llm
errors.add(:default_llm, I18n.t("discourse_ai.ai_bot.personas.default_llm_required"))
end
end
@@ -261,32 +281,36 @@ end
#
# Table name: ai_personas
#
# id :bigint not null, primary key
# name :string(100) not null
# description :string(2000) not null
# system_prompt :string(10000000) not null
# allowed_group_ids :integer default([]), not null, is an Array
# created_by_id :integer
# enabled :boolean default(TRUE), not null
# created_at :datetime not null
# updated_at :datetime not null
# system :boolean default(FALSE), not null
# priority :boolean default(FALSE), not null
# temperature :float
# top_p :float
# user_id :integer
# mentionable :boolean default(FALSE), not null
# default_llm :text
# max_context_posts :integer
# vision_enabled :boolean default(FALSE), not null
# vision_max_pixels :integer default(1048576), not null
# rag_chunk_tokens :integer default(374), not null
# rag_chunk_overlap_tokens :integer default(10), not null
# rag_conversation_chunks :integer default(10), not null
# question_consolidator_llm :text
# allow_chat :boolean default(FALSE), not null
# tool_details :boolean default(TRUE), not null
# tools :json not null
# id :bigint not null, primary key
# name :string(100) not null
# description :string(2000) not null
# system_prompt :string(10000000) not null
# allowed_group_ids :integer default([]), not null, is an Array
# created_by_id :integer
# enabled :boolean default(TRUE), not null
# created_at :datetime not null
# updated_at :datetime not null
# system :boolean default(FALSE), not null
# priority :boolean default(FALSE), not null
# temperature :float
# top_p :float
# user_id :integer
# default_llm :text
# max_context_posts :integer
# vision_enabled :boolean default(FALSE), not null
# vision_max_pixels :integer default(1048576), not null
# rag_chunk_tokens :integer default(374), not null
# rag_chunk_overlap_tokens :integer default(10), not null
# rag_conversation_chunks :integer default(10), not null
# question_consolidator_llm :text
# tool_details :boolean default(TRUE), not null
# tools :json not null
# forced_tool_count :integer default(-1), not null
# allow_chat_channel_mentions :boolean default(FALSE), not null
# allow_chat_direct_messages :boolean default(FALSE), not null
# allow_topic_mentions :boolean default(FALSE), not null
# allow_personal_messages :boolean default(TRUE), not null
# force_default_llm :boolean default(FALSE), not null
#
# Indexes
#

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