decidim-ai 0.30.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|