decidim-ai 0.30.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +47 -0
- data/Rakefile +3 -0
- data/app/jobs/decidim/ai/application_job.rb +8 -0
- data/app/jobs/decidim/ai/spam_detection/application_job.rb +17 -0
- data/app/jobs/decidim/ai/spam_detection/generic_spam_analyzer_job.rb +37 -0
- data/app/jobs/decidim/ai/spam_detection/user_spam_analyzer_job.rb +25 -0
- data/lib/decidim/ai/engine.rb +104 -0
- data/lib/decidim/ai/language/formatter.rb +17 -0
- data/lib/decidim/ai/language/language.rb +23 -0
- data/lib/decidim/ai/spam_detection/importer/database.rb +17 -0
- data/lib/decidim/ai/spam_detection/importer/file.rb +23 -0
- data/lib/decidim/ai/spam_detection/resource/base.rb +53 -0
- data/lib/decidim/ai/spam_detection/resource/collaborative_draft.rb +17 -0
- data/lib/decidim/ai/spam_detection/resource/comment.rb +17 -0
- data/lib/decidim/ai/spam_detection/resource/debate.rb +17 -0
- data/lib/decidim/ai/spam_detection/resource/initiative.rb +17 -0
- data/lib/decidim/ai/spam_detection/resource/meeting.rb +17 -0
- data/lib/decidim/ai/spam_detection/resource/proposal.rb +17 -0
- data/lib/decidim/ai/spam_detection/resource/user_base_entity.rb +35 -0
- data/lib/decidim/ai/spam_detection/service.rb +66 -0
- data/lib/decidim/ai/spam_detection/spam_detection.rb +203 -0
- data/lib/decidim/ai/spam_detection/strategy/base.rb +28 -0
- data/lib/decidim/ai/spam_detection/strategy/bayes.rb +74 -0
- data/lib/decidim/ai/strategy_registry.rb +32 -0
- data/lib/decidim/ai/test/factories.rb +1 -0
- data/lib/decidim/ai/version.rb +9 -0
- data/lib/decidim/ai.rb +13 -0
- data/lib/tasks/decidim_ai.rake +35 -0
- metadata +160 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3dfbdf71329ed49a7ff135c96047b20b05dad7746bb61885c823f7b759cec3db
|
4
|
+
data.tar.gz: 28742be322499ab899e685dc990d5ef0aa8fdc9e8e6c4fc878f22b976c1e1245
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f112e89075cb0bb1ee98f4df949ea4138a11dce85a12d8c7cb804ef116eb9f17117693fe467d05cc78990223826341297d9c0ca895c1b5fb9b66069521ee8963
|
7
|
+
data.tar.gz: 852350e6ebec0220b63829da254d9187c5ced7639d57fffa14df30cf2776ad11ba0a76b668fc4a7656f27497ec2ae57aa3d7fc463725fdb5cc705fbcf159b8dc
|
data/README.md
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# Decidim::Ai
|
2
|
+
|
3
|
+
The Decidim::Ai is a library that aims to provide Artificial Intelligence tools for Decidim. This plugin has been initially developed aiming to analyze the content and provide spam classification using Naive Bayes algorithm.
|
4
|
+
All AI related functionality provided by Decidim should be included in this same module.
|
5
|
+
|
6
|
+
For more documentation on the AI tools API, please refer to [documentation](https://docs.decidim.org/en/develop/develop/ai_tools.html)
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
In order to install use this module, you need at least Decidim 0.30 to be installed.
|
11
|
+
|
12
|
+
To install this module, run in your console:
|
13
|
+
|
14
|
+
```bash
|
15
|
+
bundle add decidim-ai
|
16
|
+
```
|
17
|
+
|
18
|
+
After that, add an initializer file as presented in the [documentation](https://docs.decidim.org/en/develop/services/aitools.html#_configuration)
|
19
|
+
|
20
|
+
Then, you need to run the below command, so that the reporting user is created.
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
bin/rails decidim:ai:spam:create_reporting_user
|
24
|
+
```
|
25
|
+
|
26
|
+
Then you can use the below command to train the engine with the module dataset:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
bin/rails decidim:ai:spam:load_module_dataset
|
30
|
+
```
|
31
|
+
|
32
|
+
Add the queue name to `config/sidekiq.yml` file:
|
33
|
+
|
34
|
+
```yaml
|
35
|
+
:queues:
|
36
|
+
- ["default", 1]
|
37
|
+
- ["spam_analysis", 1]
|
38
|
+
# The other yaml entries
|
39
|
+
```
|
40
|
+
|
41
|
+
## Contributing
|
42
|
+
|
43
|
+
See [Decidim](https://github.com/decidim/decidim).
|
44
|
+
|
45
|
+
## License
|
46
|
+
|
47
|
+
See [Decidim](https://github.com/decidim/decidim).
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Ai
|
5
|
+
module SpamDetection
|
6
|
+
class ApplicationJob < Decidim::Ai::ApplicationJob
|
7
|
+
queue_as :spam_analysis
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
def classifier
|
12
|
+
@classifier ||= Decidim::Ai::SpamDetection.resource_classifier
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Ai
|
5
|
+
module SpamDetection
|
6
|
+
class GenericSpamAnalyzerJob < ApplicationJob
|
7
|
+
include Decidim::TranslatableAttributes
|
8
|
+
|
9
|
+
def perform(reportable, author, locale, fields)
|
10
|
+
@author = author
|
11
|
+
overall_score = I18n.with_locale(locale) do
|
12
|
+
fields.map do |field|
|
13
|
+
classifier.classify(translated_attribute(reportable.send(field)))
|
14
|
+
classifier.score
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
overall_score = overall_score.inject(0.0, :+) / overall_score.size
|
19
|
+
|
20
|
+
return unless overall_score >= Decidim::Ai::SpamDetection.resource_score_threshold
|
21
|
+
|
22
|
+
Decidim::CreateReport.call(form, reportable)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def form
|
28
|
+
@form ||= Decidim::ReportForm.new(reason: "spam", details: classifier.classification_log).with_context(current_user: reporting_user)
|
29
|
+
end
|
30
|
+
|
31
|
+
def reporting_user
|
32
|
+
@reporting_user ||= Decidim::User.find_by!(email: Decidim::Ai::SpamDetection.reporting_user_email)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Ai
|
5
|
+
module SpamDetection
|
6
|
+
class UserSpamAnalyzerJob < GenericSpamAnalyzerJob
|
7
|
+
def perform(reportable)
|
8
|
+
@author = reportable
|
9
|
+
|
10
|
+
classifier.classify(reportable.about)
|
11
|
+
|
12
|
+
return unless classifier.score >= Decidim::Ai::SpamDetection.user_score_threshold
|
13
|
+
|
14
|
+
Decidim::CreateUserReport.call(form, reportable)
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def classifier
|
20
|
+
@classifier ||= Decidim::Ai::SpamDetection.user_classifier
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Ai
|
5
|
+
class Engine < ::Rails::Engine
|
6
|
+
isolate_namespace Decidim::Ai
|
7
|
+
|
8
|
+
paths["db/migrate"] = nil
|
9
|
+
|
10
|
+
initializer "decidim_ai.resource_classifiers" do |_app|
|
11
|
+
Decidim::Ai::SpamDetection.resource_analyzers.each do |analyzer|
|
12
|
+
Decidim::Ai::SpamDetection.resource_registry.register_analyzer(**analyzer)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
initializer "decidim_ai.user_classifiers" do |_app|
|
17
|
+
Decidim::Ai::SpamDetection.user_analyzers.each do |analyzer|
|
18
|
+
Decidim::Ai::SpamDetection.user_registry.register_analyzer(**analyzer)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
initializer "decidim_ai.events.subscribe_profile" do
|
23
|
+
config.to_prepare do
|
24
|
+
Decidim::EventsManager.subscribe("decidim.update_account:after") do |_event_name, data|
|
25
|
+
Decidim::Ai::SpamDetection::UserSpamAnalyzerJob.perform_later(data[:resource])
|
26
|
+
end
|
27
|
+
Decidim::EventsManager.subscribe("decidim.update_user_group:after") do |_event_name, data|
|
28
|
+
Decidim::Ai::SpamDetection::UserSpamAnalyzerJob.perform_later(data[:resource])
|
29
|
+
end
|
30
|
+
Decidim::EventsManager.subscribe("decidim.create_user_group:after") do |_event_name, data|
|
31
|
+
Decidim::Ai::SpamDetection::UserSpamAnalyzerJob.perform_later(data[:resource])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
initializer "decidim_ai.events.subscribe_comments" do
|
37
|
+
config.to_prepare do
|
38
|
+
ActiveSupport::Notifications.subscribe("decidim.comments.create_comment:after") do |_event_name, data|
|
39
|
+
Decidim::Ai::SpamDetection::GenericSpamAnalyzerJob.perform_later(data[:resource], data.dig(:extra, :event_author), data.dig(:extra, :locale), [:body])
|
40
|
+
end
|
41
|
+
ActiveSupport::Notifications.subscribe("decidim.comments.update_comment:after") do |_event_name, data|
|
42
|
+
Decidim::Ai::SpamDetection::GenericSpamAnalyzerJob.perform_later(data[:resource], data.dig(:extra, :event_author), data.dig(:extra, :locale), [:body])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
initializer "decidim_ai.events.subscribe_meeting" do
|
48
|
+
config.to_prepare do
|
49
|
+
ActiveSupport::Notifications.subscribe("decidim.meetings.create_meeting:after") do |_event_name, data|
|
50
|
+
Decidim::Ai::SpamDetection::GenericSpamAnalyzerJob.perform_later(data[:resource], data.dig(:extra, :event_author), data.dig(:extra, :locale),
|
51
|
+
[:description, :title, :location_hints, :registration_terms])
|
52
|
+
end
|
53
|
+
ActiveSupport::Notifications.subscribe("decidim.meetings.update_meeting:after") do |_event_name, data|
|
54
|
+
Decidim::Ai::SpamDetection::GenericSpamAnalyzerJob.perform_later(data[:resource], data.dig(:extra, :event_author), data.dig(:extra, :locale),
|
55
|
+
[:description, :title, :location_hints, :registration_terms])
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
initializer "decidim_ai.events.subscribe_debate" do
|
61
|
+
config.to_prepare do
|
62
|
+
ActiveSupport::Notifications.subscribe("decidim.debates.create_debate:after") do |_event_name, data|
|
63
|
+
Decidim::Ai::SpamDetection::GenericSpamAnalyzerJob.perform_later(data[:resource], data.dig(:extra, :event_author), data.dig(:extra, :locale), [:description, :title])
|
64
|
+
end
|
65
|
+
ActiveSupport::Notifications.subscribe("decidim.debates.update_debate:after") do |_event_name, data|
|
66
|
+
Decidim::Ai::SpamDetection::GenericSpamAnalyzerJob.perform_later(data[:resource], data.dig(:extra, :event_author), data.dig(:extra, :locale), [:description, :title])
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
initializer "decidim_ai.events.subscribe_initiatives" do
|
72
|
+
config.to_prepare do
|
73
|
+
ActiveSupport::Notifications.subscribe("decidim.initiatives.create_initiative:after") do |_event_name, data|
|
74
|
+
Decidim::Ai::SpamDetection::GenericSpamAnalyzerJob.perform_later(data[:resource], data.dig(:extra, :event_author), data.dig(:extra, :locale), [:description, :title])
|
75
|
+
end
|
76
|
+
ActiveSupport::Notifications.subscribe("decidim.initiatives.update_initiative:after") do |_event_name, data|
|
77
|
+
Decidim::Ai::SpamDetection::GenericSpamAnalyzerJob.perform_later(data[:resource], data.dig(:extra, :event_author), data.dig(:extra, :locale), [:description, :title])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
initializer "decidim_ai.events.subscribe_proposals" do
|
83
|
+
config.to_prepare do
|
84
|
+
ActiveSupport::Notifications.subscribe("decidim.proposals.create_proposal:after") do |_event_name, data|
|
85
|
+
Decidim::Ai::SpamDetection::GenericSpamAnalyzerJob.perform_later(data[:resource], data.dig(:extra, :event_author), data.dig(:extra, :locale), [:body, :title])
|
86
|
+
end
|
87
|
+
ActiveSupport::Notifications.subscribe("decidim.proposals.update_proposal:after") do |_event_name, data|
|
88
|
+
Decidim::Ai::SpamDetection::GenericSpamAnalyzerJob.perform_later(data[:resource], data.dig(:extra, :event_author), data.dig(:extra, :locale), [:body, :title])
|
89
|
+
end
|
90
|
+
ActiveSupport::Notifications.subscribe("decidim.proposals.create_collaborative_draft:after") do |_event_name, data|
|
91
|
+
Decidim::Ai::SpamDetection::GenericSpamAnalyzerJob.perform_later(data[:resource], data.dig(:extra, :event_author), data.dig(:extra, :locale), [:body, :title])
|
92
|
+
end
|
93
|
+
ActiveSupport::Notifications.subscribe("decidim.proposals.update_collaborative_draft:after") do |_event_name, data|
|
94
|
+
Decidim::Ai::SpamDetection::GenericSpamAnalyzerJob.perform_later(data[:resource], data.dig(:extra, :event_author), data.dig(:extra, :locale), [:body, :title])
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def load_seed
|
100
|
+
nil
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Ai
|
5
|
+
module Language
|
6
|
+
class Formatter
|
7
|
+
include ActionView::Helpers::SanitizeHelper
|
8
|
+
|
9
|
+
# for the moment, we just use strip_tags to clean-up the text. At a later stage, we may need to introduce
|
10
|
+
# stemmers, ngrams or other kind of text normalization, as well any language specific criteria
|
11
|
+
def cleanup(text)
|
12
|
+
strip_tags(text)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Ai
|
5
|
+
module Language
|
6
|
+
autoload :Formatter, "decidim/ai/language/formatter"
|
7
|
+
include ActiveSupport::Configurable
|
8
|
+
|
9
|
+
# Text cleanup service
|
10
|
+
#
|
11
|
+
# If you want to implement your own text formatter, you can use a class having the following contract
|
12
|
+
#
|
13
|
+
# class Formatter
|
14
|
+
# def cleanup(text)
|
15
|
+
# # your code
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
config_accessor :formatter do
|
19
|
+
"Decidim::Ai::Language::Formatter"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Ai
|
5
|
+
module SpamDetection
|
6
|
+
module Importer
|
7
|
+
class Database
|
8
|
+
def self.call
|
9
|
+
Decidim::Ai::SpamDetection.resource_models.values.each do |model|
|
10
|
+
model.constantize.new.batch_train
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Ai
|
5
|
+
module SpamDetection
|
6
|
+
module Importer
|
7
|
+
class File
|
8
|
+
def self.call(file, service)
|
9
|
+
ext = ::File.extname(file)[1..-1]
|
10
|
+
reader_class = Decidim::Admin::Import::Readers.search_by_file_extension(ext)
|
11
|
+
|
12
|
+
reader_class.new(file).read_rows do |row|
|
13
|
+
next unless [:spam, :ham].include?(row[0].to_sym)
|
14
|
+
next if row[1].blank?
|
15
|
+
|
16
|
+
service.train(row[0].to_sym, row[1])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Ai
|
5
|
+
module SpamDetection
|
6
|
+
module Resource
|
7
|
+
class Base
|
8
|
+
include Decidim::TranslatableAttributes
|
9
|
+
|
10
|
+
def fields; end
|
11
|
+
|
12
|
+
def batch_train
|
13
|
+
query.find_each(batch_size: 100) do |resource|
|
14
|
+
classification = resource_hidden?(resource) ? :spam : :ham
|
15
|
+
|
16
|
+
fields.each do |field_name|
|
17
|
+
raise "#{resource.class.name} does not implement #{field_name} as defined in `#{self.class.name}`" unless resource.respond_to?(field_name.to_sym)
|
18
|
+
|
19
|
+
train classification, translated_attribute(resource.send(field_name.to_sym))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def train(category, text)
|
25
|
+
raise error_message("Decidim::Ai::SpamDetection.resource_detection_service", __method__) unless classifier.respond_to?(:train)
|
26
|
+
|
27
|
+
classifier.train(category, text)
|
28
|
+
end
|
29
|
+
|
30
|
+
def untrain(category, text)
|
31
|
+
raise error_message("Decidim::Ai::SpamDetection.resource_detection_service", __method__) unless classifier.respond_to?(:untrain)
|
32
|
+
|
33
|
+
classifier.untrain(category, text)
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def error_message(klass, method_name)
|
39
|
+
"Invalid Classifier class! The class defined under `#{klass}` does not follow the contract regarding ##{method_name} method"
|
40
|
+
end
|
41
|
+
|
42
|
+
def resource_hidden?(resource)
|
43
|
+
resource.class.included_modules.include?(Decidim::Reportable) && resource.hidden?
|
44
|
+
end
|
45
|
+
|
46
|
+
def classifier
|
47
|
+
@classifier ||= Decidim::Ai::SpamDetection.resource_classifier
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Ai
|
5
|
+
module SpamDetection
|
6
|
+
module Resource
|
7
|
+
class CollaborativeDraft < Base
|
8
|
+
def fields = [:body, :title]
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def query = Decidim::Proposals::CollaborativeDraft.includes(:moderation)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Ai
|
5
|
+
module SpamDetection
|
6
|
+
module Resource
|
7
|
+
class Comment < Base
|
8
|
+
def fields = [:body]
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def query = Decidim::Comments::Comment.includes(:moderation)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Ai
|
5
|
+
module SpamDetection
|
6
|
+
module Resource
|
7
|
+
class Debate < Base
|
8
|
+
def fields = [:description, :title]
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def query = Decidim::Debates::Debate.includes(:moderation)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Ai
|
5
|
+
module SpamDetection
|
6
|
+
module Resource
|
7
|
+
class Initiative < Base
|
8
|
+
def fields = [:description, :title]
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def query = Decidim::Initiative
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Ai
|
5
|
+
module SpamDetection
|
6
|
+
module Resource
|
7
|
+
class Meeting < Base
|
8
|
+
def fields = [:description, :title, :location_hints, :registration_terms, :closing_report]
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def query = Decidim::Meetings::Meeting.includes(:moderation)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Ai
|
5
|
+
module SpamDetection
|
6
|
+
module Resource
|
7
|
+
class Proposal < Base
|
8
|
+
def fields = [:body, :title]
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def query = Decidim::Proposals::Proposal.includes(:moderation)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Ai
|
5
|
+
module SpamDetection
|
6
|
+
module Resource
|
7
|
+
class UserBaseEntity < Base
|
8
|
+
def fields = [:about]
|
9
|
+
|
10
|
+
def train(category, text)
|
11
|
+
raise error_message("Decidim::Ai::SpamDetection.user_detection_service", __method__) unless classifier.respond_to?(:train)
|
12
|
+
|
13
|
+
classifier.train(category, text)
|
14
|
+
end
|
15
|
+
|
16
|
+
def untrain(category, text)
|
17
|
+
raise error_message("Decidim::Ai::SpamDetection.user_detection_service", __method__) unless classifier.respond_to?(:untrain)
|
18
|
+
|
19
|
+
classifier.untrain(category, text)
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
|
24
|
+
def query = Decidim::UserBaseEntity
|
25
|
+
|
26
|
+
def resource_hidden?(resource) = resource.class.included_modules.include?(Decidim::UserReportable) && resource.blocked?
|
27
|
+
|
28
|
+
def classifier
|
29
|
+
@classifier ||= Decidim::Ai::SpamDetection.user_classifier
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Ai
|
5
|
+
module SpamDetection
|
6
|
+
class Service
|
7
|
+
def initialize(registry:)
|
8
|
+
@registry = registry
|
9
|
+
end
|
10
|
+
|
11
|
+
def reset
|
12
|
+
@registry.each do |strategy|
|
13
|
+
next unless strategy.respond_to?(:reset)
|
14
|
+
|
15
|
+
strategy.reset
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def train(category, text)
|
20
|
+
text = formatter.cleanup(text)
|
21
|
+
return if text.blank?
|
22
|
+
|
23
|
+
@registry.each do |strategy|
|
24
|
+
strategy.train(category, text)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def classify(text)
|
29
|
+
text = formatter.cleanup(text)
|
30
|
+
return if text.blank?
|
31
|
+
|
32
|
+
@registry.each do |strategy|
|
33
|
+
strategy.classify(text)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def untrain(category, text)
|
38
|
+
text = formatter.cleanup(text)
|
39
|
+
return if text.blank?
|
40
|
+
|
41
|
+
@registry.each do |strategy|
|
42
|
+
strategy.untrain(category, text)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def score
|
47
|
+
@registry.collect(&:score).inject(0.0, :+) / @registry.size
|
48
|
+
end
|
49
|
+
|
50
|
+
def classification_log
|
51
|
+
@classification_log = []
|
52
|
+
@registry.each do |strategy|
|
53
|
+
@classification_log << strategy.log
|
54
|
+
end
|
55
|
+
@classification_log.join("\n")
|
56
|
+
end
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
def formatter
|
61
|
+
@formatter ||= Decidim::Ai::Language.formatter.safe_constantize&.new
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Ai
|
5
|
+
module SpamDetection
|
6
|
+
include ActiveSupport::Configurable
|
7
|
+
|
8
|
+
autoload :Service, "decidim/ai/spam_detection/service"
|
9
|
+
|
10
|
+
module Resource
|
11
|
+
autoload :Base, "decidim/ai/spam_detection/resource/base"
|
12
|
+
autoload :Comment, "decidim/ai/spam_detection/resource/comment"
|
13
|
+
autoload :Debate, "decidim/ai/spam_detection/resource/debate"
|
14
|
+
autoload :Initiative, "decidim/ai/spam_detection/resource/initiative"
|
15
|
+
autoload :Proposal, "decidim/ai/spam_detection/resource/proposal"
|
16
|
+
autoload :CollaborativeDraft, "decidim/ai/spam_detection/resource/collaborative_draft"
|
17
|
+
autoload :Meeting, "decidim/ai/spam_detection/resource/meeting"
|
18
|
+
autoload :UserBaseEntity, "decidim/ai/spam_detection/resource/user_base_entity"
|
19
|
+
end
|
20
|
+
|
21
|
+
module Importer
|
22
|
+
autoload :File, "decidim/ai/spam_detection/importer/file"
|
23
|
+
autoload :Database, "decidim/ai/spam_detection/importer/database"
|
24
|
+
end
|
25
|
+
|
26
|
+
module Strategy
|
27
|
+
autoload :Base, "decidim/ai/spam_detection/strategy/base"
|
28
|
+
autoload :Bayes, "decidim/ai/spam_detection/strategy/bayes"
|
29
|
+
end
|
30
|
+
|
31
|
+
# This is the email address used by the spam engine to
|
32
|
+
# properly identify the user that will report users and content
|
33
|
+
config_accessor :reporting_user_email do
|
34
|
+
"decidim-reporting-user@example.org"
|
35
|
+
end
|
36
|
+
|
37
|
+
# You can configure the spam threshold for the spam detection service.
|
38
|
+
# The threshold is a float value between 0 and 1.
|
39
|
+
# The default value is 0.75
|
40
|
+
# Any value below the threshold will be considered spam.
|
41
|
+
config_accessor :resource_score_threshold do
|
42
|
+
0.75
|
43
|
+
end
|
44
|
+
|
45
|
+
# Registered analyzers.
|
46
|
+
# You can register your own analyzer by adding a new entry to this array.
|
47
|
+
# The entry must be a hash with the following keys:
|
48
|
+
# - name: the name of the analyzer
|
49
|
+
# - strategy: the class of the strategy to use
|
50
|
+
# - options: a hash with the options to pass to the strategy
|
51
|
+
# Example:
|
52
|
+
# config.resource_analyzers = {
|
53
|
+
# name: :bayes,
|
54
|
+
# strategy: Decidim::Ai::SpamContent::BayesStrategy,
|
55
|
+
# options: {
|
56
|
+
# adapter: :redis,
|
57
|
+
# params: {
|
58
|
+
# url: ENV["DECIDIM_SPAM_DETECTION_BACKEND_RESOURCE_URL"]
|
59
|
+
# }
|
60
|
+
# }
|
61
|
+
# }
|
62
|
+
config_accessor :resource_analyzers do
|
63
|
+
[
|
64
|
+
{
|
65
|
+
name: :bayes,
|
66
|
+
strategy: Decidim::Ai::SpamDetection::Strategy::Bayes,
|
67
|
+
options: {
|
68
|
+
adapter: ENV.fetch("DECIDIM_SPAM_DETECTION_BACKEND_RESOURCE", "redis"),
|
69
|
+
params: { url: ENV.fetch("DECIDIM_SPAM_DETECTION_BACKEND_RESOURCE_URL", "redis://localhost:6379/2") }
|
70
|
+
}
|
71
|
+
}
|
72
|
+
]
|
73
|
+
end
|
74
|
+
|
75
|
+
# This config_accessor allows the implementers to change the class being used by the classifier,
|
76
|
+
# in order to change the finder method. or even define own resource visibility criteria.
|
77
|
+
# This is the place where new resources can be registered following the pattern
|
78
|
+
# Resource => Handler
|
79
|
+
config_accessor :resource_models do
|
80
|
+
@models ||= begin
|
81
|
+
models = {}
|
82
|
+
models["Decidim::Comments::Comment"] = "Decidim::Ai::SpamDetection::Resource::Comment" if Decidim.module_installed?("comments")
|
83
|
+
models["Decidim::Debates::Debate"] = "Decidim::Ai::SpamDetection::Resource::Debate" if Decidim.module_installed?("debates")
|
84
|
+
models["Decidim::Initiative"] = "Decidim::Ai::SpamDetection::Resource::Initiative" if Decidim.module_installed?("initiatives")
|
85
|
+
models["Decidim::Meetings::Meeting"] = "Decidim::Ai::SpamDetection::Resource::Meeting" if Decidim.module_installed?("meetings")
|
86
|
+
models["Decidim::Proposals::Proposal"] = "Decidim::Ai::SpamDetection::Resource::Proposal" if Decidim.module_installed?("proposals")
|
87
|
+
models["Decidim::Proposals::CollaborativeDraft"] = "Decidim::Ai::SpamDetection::Resource::CollaborativeDraft" if Decidim.module_installed?("proposals")
|
88
|
+
models
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Spam detection service class.
|
93
|
+
# If you want to use a different spam detection service, you can use a class service having the following contract
|
94
|
+
config_accessor :resource_detection_service do
|
95
|
+
"Decidim::Ai::SpamDetection::Service"
|
96
|
+
end
|
97
|
+
|
98
|
+
# You can configure the spam threshold for the spam detection service.
|
99
|
+
# The threshold is a float value between 0 and 1.
|
100
|
+
# The default value is 0.75
|
101
|
+
# Any value below the threshold will be considered spam.
|
102
|
+
config_accessor :user_score_threshold do
|
103
|
+
0.75
|
104
|
+
end
|
105
|
+
|
106
|
+
# Registered analyzers.
|
107
|
+
# You can register your own analyzer by adding a new entry to this array.
|
108
|
+
# The entry must be a hash with the following keys:
|
109
|
+
# - name: the name of the analyzer
|
110
|
+
# - strategy: the class of the strategy to use
|
111
|
+
# - options: a hash with the options to pass to the strategy
|
112
|
+
# Example:
|
113
|
+
# config.user_analyzers = {
|
114
|
+
# name: :bayes,
|
115
|
+
# strategy: Decidim::Ai::SpamContent::BayesStrategy,
|
116
|
+
# options: {
|
117
|
+
# adapter: :redis,
|
118
|
+
# params: {
|
119
|
+
# url: ENV["DECIDIM_SPAM_DETECTION_BACKEND_USER_REDIS_URL"]
|
120
|
+
# }
|
121
|
+
# }
|
122
|
+
# }
|
123
|
+
config_accessor :user_analyzers do
|
124
|
+
[
|
125
|
+
{
|
126
|
+
name: :bayes,
|
127
|
+
strategy: Decidim::Ai::SpamDetection::Strategy::Bayes,
|
128
|
+
options: {
|
129
|
+
adapter: ENV.fetch("DECIDIM_SPAM_DETECTION_BACKEND_USER", "redis"),
|
130
|
+
params: { url: ENV.fetch("DECIDIM_SPAM_DETECTION_BACKEND_USER_REDIS_URL", "redis://localhost:6379/3") }
|
131
|
+
}
|
132
|
+
}
|
133
|
+
]
|
134
|
+
end
|
135
|
+
|
136
|
+
# This config_accessor allows the implementers to change the class being used by the classifier,
|
137
|
+
# in order to change the finder method or what a hidden user really is.
|
138
|
+
# The same applies for UserGroups.
|
139
|
+
config_accessor :user_models do
|
140
|
+
@user_models ||= begin
|
141
|
+
user_models = {}
|
142
|
+
|
143
|
+
user_models["Decidim::UserGroup"] = "Decidim::Ai::SpamDetection::Resource::UserBaseEntity"
|
144
|
+
user_models["Decidim::User"] = "Decidim::Ai::SpamDetection::Resource::UserBaseEntity"
|
145
|
+
user_models
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Spam detection service class.
|
150
|
+
# If you want to use a different spam detection service, you can use a class service having the following contract
|
151
|
+
config_accessor :user_detection_service do
|
152
|
+
"Decidim::Ai::SpamDetection::Service"
|
153
|
+
end
|
154
|
+
|
155
|
+
# this is the generic resource classifier class. If you need to change your own class, please change the
|
156
|
+
# configuration of `Decidim::Ai::SpamDetection.detection_service` variable.
|
157
|
+
def self.resource_classifier
|
158
|
+
@resource_classifier = Decidim::Ai::SpamDetection.resource_detection_service.safe_constantize&.new(
|
159
|
+
registry: Decidim::Ai::SpamDetection.resource_registry
|
160
|
+
)
|
161
|
+
end
|
162
|
+
|
163
|
+
# The registry instance that stores the list of strategies needed to process the resources
|
164
|
+
# In essence is an enumerator class that responds to `register_analyzer(**params)` and `for(name)` methods
|
165
|
+
def self.resource_registry
|
166
|
+
@resource_registry ||= Decidim::Ai::StrategyRegistry.new
|
167
|
+
end
|
168
|
+
|
169
|
+
# this is the generic user classifier class. If you need to change your own class, please change the
|
170
|
+
# configuration of `Decidim::Ai::SpamDetection.detection_service` variable
|
171
|
+
def self.user_classifier
|
172
|
+
@user_classifier = Decidim::Ai::SpamDetection.user_detection_service.safe_constantize&.new(
|
173
|
+
registry: Decidim::Ai::SpamDetection.user_registry
|
174
|
+
)
|
175
|
+
end
|
176
|
+
|
177
|
+
# The registry instance that stores the list of strategies needed to process the user objects
|
178
|
+
# In essence is an enumerator class that responds to `register_analyzer(**params)` and `for(name)` methods
|
179
|
+
def self.user_registry
|
180
|
+
@user_registry ||= Decidim::Ai::StrategyRegistry.new
|
181
|
+
end
|
182
|
+
|
183
|
+
# This method is being called to ensure that user with email configured in
|
184
|
+
# `Decidim::Ai::SpamDetection.reporting_user_email` variable exists in the database.
|
185
|
+
def self.create_reporting_user!
|
186
|
+
Decidim::Organization.find_each do |organization|
|
187
|
+
user = organization.users.find_or_initialize_by(email: Decidim::Ai::SpamDetection.reporting_user_email)
|
188
|
+
next if user.persisted?
|
189
|
+
|
190
|
+
password = SecureRandom.hex(10)
|
191
|
+
user.password = password
|
192
|
+
user.password_confirmation = password
|
193
|
+
|
194
|
+
user.deleted_at = Time.current
|
195
|
+
user.tos_agreement = true
|
196
|
+
user.name = ""
|
197
|
+
user.skip_confirmation!
|
198
|
+
user.save!
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Ai
|
5
|
+
module SpamDetection
|
6
|
+
module Strategy
|
7
|
+
class Base
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
@name = options.delete(:name)
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def classify(_content); end
|
16
|
+
|
17
|
+
def train(_classification, _content); end
|
18
|
+
|
19
|
+
def untrain(_classification, _content); end
|
20
|
+
|
21
|
+
def log; end
|
22
|
+
|
23
|
+
def score = 0.0
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "classifier-reborn"
|
4
|
+
|
5
|
+
module Decidim
|
6
|
+
module Ai
|
7
|
+
module SpamDetection
|
8
|
+
module Strategy
|
9
|
+
class Bayes < Base
|
10
|
+
def initialize(options = {})
|
11
|
+
super
|
12
|
+
@options = { adapter: :memory, categories: [:ham, :spam], params: {} }.deep_merge(options)
|
13
|
+
|
14
|
+
@available_categories = options[:categories]
|
15
|
+
@backend = ClassifierReborn::Bayes.new(*available_categories, backend: configured_backend)
|
16
|
+
end
|
17
|
+
|
18
|
+
def log
|
19
|
+
return unless category
|
20
|
+
|
21
|
+
"The Classification engine marked this as #{category}"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Calling this method without any trained categories will throw an error
|
25
|
+
def untrain(category, content)
|
26
|
+
return unless backend.categories.collect(&:downcase).collect(&:to_sym).include?(category)
|
27
|
+
|
28
|
+
backend.untrain(category, content)
|
29
|
+
end
|
30
|
+
|
31
|
+
delegate :train, :reset, to: :backend
|
32
|
+
|
33
|
+
def classify(content)
|
34
|
+
@category, @internal_score = backend.classify_with_score(content)
|
35
|
+
category
|
36
|
+
end
|
37
|
+
|
38
|
+
# The Bayes strategy returns a score between that can be lower than -1
|
39
|
+
# As per ClassifierReborn documentation, closest to 0 is being picked as the dominant category
|
40
|
+
#
|
41
|
+
# From original documentation:
|
42
|
+
# Returns the scores in each category the provided +text+. E.g.,
|
43
|
+
# b.classifications "I hate bad words and you"
|
44
|
+
# => {"Uninteresting"=>-12.6997928013932, "Interesting"=>-18.4206807439524}
|
45
|
+
# The largest of these scores (the one closest to 0) is the one picked out by #classify
|
46
|
+
def score
|
47
|
+
category.presence == "spam" ? 1 : 0
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
attr_reader :backend, :options, :available_categories, :category, :internal_score
|
53
|
+
|
54
|
+
def configured_backend
|
55
|
+
if options[:adapter].to_s == "memory"
|
56
|
+
system_log "[decidim-ai] #{self.class.name} - Running the Memory backend as it was requested. This is not recommended for production environment."
|
57
|
+
ClassifierReborn::BayesMemoryBackend.new
|
58
|
+
elsif options.dig(:params, :url) && options.dig(:params, :url).empty?
|
59
|
+
system_log "[decidim-ai] #{self.class.name} - Running the Memory backend as there are no redis credentials. This is not recommended for production environment."
|
60
|
+
ClassifierReborn::BayesMemoryBackend.new
|
61
|
+
else
|
62
|
+
system_log "[decidim-ai] #{self.class.name} - Running the Redis backend"
|
63
|
+
ClassifierReborn::BayesRedisBackend.new options[:params]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def system_log(message)
|
68
|
+
Rails.logger.info message
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Ai
|
5
|
+
class StrategyRegistry
|
6
|
+
class StrategyAlreadyRegistered < StandardError; end
|
7
|
+
|
8
|
+
delegate :clear, :collect, :each, :size, to: :strategies
|
9
|
+
attr_reader :strategies
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@strategies = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def register_analyzer(name:, strategy:, options: {})
|
16
|
+
if self.for(name).present?
|
17
|
+
raise(
|
18
|
+
StrategyAlreadyRegistered,
|
19
|
+
"There is a strategy already registered with the name `:#{name}`"
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
options = { name: }.merge(options)
|
24
|
+
strategies << strategy.new(options)
|
25
|
+
end
|
26
|
+
|
27
|
+
def for(name)
|
28
|
+
strategies.select { |k, _v| k.name == name }.first
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
# frozen_string_literal: true
|
data/lib/decidim/ai.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "decidim/ai/engine"
|
4
|
+
|
5
|
+
module Decidim
|
6
|
+
module Ai
|
7
|
+
autoload :StrategyRegistry, "decidim/ai/strategy_registry"
|
8
|
+
autoload :SpamDetection, "decidim/ai/spam_detection/spam_detection"
|
9
|
+
autoload :Language, "decidim/ai/language/language"
|
10
|
+
|
11
|
+
include ActiveSupport::Configurable
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :decidim do
|
4
|
+
namespace :ai do
|
5
|
+
namespace :spam do
|
6
|
+
desc "Create reporting user"
|
7
|
+
task create_reporting_user: :environment do
|
8
|
+
Decidim::Ai::SpamDetection.create_reporting_user!
|
9
|
+
end
|
10
|
+
|
11
|
+
desc "Load application dataset file"
|
12
|
+
task :load_application_dataset, [:file] => :environment do |_, args|
|
13
|
+
Decidim::Ai::SpamDetection::Importer::File.call(args[:file], Decidim::Ai::SpamDetection.user_classifier)
|
14
|
+
Decidim::Ai::SpamDetection::Importer::File.call(args[:file], Decidim::Ai::SpamDetection.resource_classifier)
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Train model using application database"
|
18
|
+
task train_application_database: :environment do
|
19
|
+
Decidim::Ai::SpamDetection::Importer::Database.call
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "Reset all training model"
|
23
|
+
task reset: :environment do
|
24
|
+
Decidim::Ai::SpamDetection.user_classifier.reset
|
25
|
+
Decidim::Ai::SpamDetection.resource_classifier.reset
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def plugin_path
|
31
|
+
Gem.loaded_specs["decidim-ai"].full_gem_path
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
metadata
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: decidim-ai
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.30.0.rc1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alexandru-Emil Lupu
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-02-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: classifier-reborn
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.3.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.3.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: decidim-core
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.30.0.rc1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.30.0.rc1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: decidim-debates
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.30.0.rc1
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.30.0.rc1
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: decidim-initiatives
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.30.0.rc1
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.30.0.rc1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: decidim-meetings
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.30.0.rc1
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.30.0.rc1
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: decidim-proposals
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.30.0.rc1
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.30.0.rc1
|
97
|
+
description: A module that aims to provide Artificial Intelligence tools for Decidim.
|
98
|
+
email:
|
99
|
+
- contact@alecslupu.ro
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- README.md
|
105
|
+
- Rakefile
|
106
|
+
- app/jobs/decidim/ai/application_job.rb
|
107
|
+
- app/jobs/decidim/ai/spam_detection/application_job.rb
|
108
|
+
- app/jobs/decidim/ai/spam_detection/generic_spam_analyzer_job.rb
|
109
|
+
- app/jobs/decidim/ai/spam_detection/user_spam_analyzer_job.rb
|
110
|
+
- lib/decidim/ai.rb
|
111
|
+
- lib/decidim/ai/engine.rb
|
112
|
+
- lib/decidim/ai/language/formatter.rb
|
113
|
+
- lib/decidim/ai/language/language.rb
|
114
|
+
- lib/decidim/ai/spam_detection/importer/database.rb
|
115
|
+
- lib/decidim/ai/spam_detection/importer/file.rb
|
116
|
+
- lib/decidim/ai/spam_detection/resource/base.rb
|
117
|
+
- lib/decidim/ai/spam_detection/resource/collaborative_draft.rb
|
118
|
+
- lib/decidim/ai/spam_detection/resource/comment.rb
|
119
|
+
- lib/decidim/ai/spam_detection/resource/debate.rb
|
120
|
+
- lib/decidim/ai/spam_detection/resource/initiative.rb
|
121
|
+
- lib/decidim/ai/spam_detection/resource/meeting.rb
|
122
|
+
- lib/decidim/ai/spam_detection/resource/proposal.rb
|
123
|
+
- lib/decidim/ai/spam_detection/resource/user_base_entity.rb
|
124
|
+
- lib/decidim/ai/spam_detection/service.rb
|
125
|
+
- lib/decidim/ai/spam_detection/spam_detection.rb
|
126
|
+
- lib/decidim/ai/spam_detection/strategy/base.rb
|
127
|
+
- lib/decidim/ai/spam_detection/strategy/bayes.rb
|
128
|
+
- lib/decidim/ai/strategy_registry.rb
|
129
|
+
- lib/decidim/ai/test/factories.rb
|
130
|
+
- lib/decidim/ai/version.rb
|
131
|
+
- lib/tasks/decidim_ai.rake
|
132
|
+
homepage: https://decidim.org
|
133
|
+
licenses:
|
134
|
+
- AGPL-3.0-or-later
|
135
|
+
metadata:
|
136
|
+
bug_tracker_uri: https://github.com/decidim/decidim/issues
|
137
|
+
documentation_uri: https://docs.decidim.org/
|
138
|
+
funding_uri: https://opencollective.com/decidim
|
139
|
+
homepage_uri: https://decidim.org
|
140
|
+
source_code_uri: https://github.com/decidim/decidim
|
141
|
+
post_install_message:
|
142
|
+
rdoc_options: []
|
143
|
+
require_paths:
|
144
|
+
- lib
|
145
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
146
|
+
requirements:
|
147
|
+
- - "~>"
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: 3.3.0
|
150
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
152
|
+
- - ">="
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '0'
|
155
|
+
requirements: []
|
156
|
+
rubygems_version: 3.5.11
|
157
|
+
signing_key:
|
158
|
+
specification_version: 4
|
159
|
+
summary: A Decidim module with AI tools
|
160
|
+
test_files: []
|