aven 0.0.3

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.
Files changed (159) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +35 -0
  4. data/Rakefile +19 -0
  5. data/app/assets/stylesheets/aven/application.css +14 -0
  6. data/app/assets/stylesheets/aven/application.tailwind.css +7 -0
  7. data/app/assets/stylesheets/aven/tailwind.css +224 -0
  8. data/app/channels/aven/chat/thread_channel.rb +39 -0
  9. data/app/components/aven/application_view_component.rb +15 -0
  10. data/app/components/aven/views/admin/dashboard/index/component.html.erb +1 -0
  11. data/app/components/aven/views/admin/dashboard/index/component.rb +5 -0
  12. data/app/components/aven/views/articles/edit/component.html.erb +14 -0
  13. data/app/components/aven/views/articles/edit/component.rb +14 -0
  14. data/app/components/aven/views/articles/form/component.html.erb +45 -0
  15. data/app/components/aven/views/articles/form/component.rb +27 -0
  16. data/app/components/aven/views/articles/index/component.html.erb +93 -0
  17. data/app/components/aven/views/articles/index/component.rb +29 -0
  18. data/app/components/aven/views/articles/new/component.html.erb +13 -0
  19. data/app/components/aven/views/articles/new/component.rb +14 -0
  20. data/app/components/aven/views/articles/show/component.html.erb +110 -0
  21. data/app/components/aven/views/articles/show/component.rb +34 -0
  22. data/app/components/aven/views/oauth/error/component.html.erb +44 -0
  23. data/app/components/aven/views/oauth/error/component.rb +30 -0
  24. data/app/components/aven/views/static/index/component.html.erb +17 -0
  25. data/app/components/aven/views/static/index/component.rb +16 -0
  26. data/app/components/aven/views/static/index/controller.js +7 -0
  27. data/app/controllers/aven/admin/base.rb +16 -0
  28. data/app/controllers/aven/admin/dashboard_controller.rb +9 -0
  29. data/app/controllers/aven/agentic/agents_controller.rb +56 -0
  30. data/app/controllers/aven/agentic/documents_controller.rb +51 -0
  31. data/app/controllers/aven/agentic/mcp_controller.rb +124 -0
  32. data/app/controllers/aven/agentic/tools_controller.rb +37 -0
  33. data/app/controllers/aven/ai/text_controller.rb +41 -0
  34. data/app/controllers/aven/application_controller.rb +27 -0
  35. data/app/controllers/aven/articles_controller.rb +114 -0
  36. data/app/controllers/aven/auth_controller.rb +12 -0
  37. data/app/controllers/aven/chat/threads_controller.rb +67 -0
  38. data/app/controllers/aven/oauth/auth0_controller.rb +84 -0
  39. data/app/controllers/aven/oauth/base_controller.rb +183 -0
  40. data/app/controllers/aven/oauth/documentation/auth0.md +387 -0
  41. data/app/controllers/aven/oauth/documentation/entra_id.md +608 -0
  42. data/app/controllers/aven/oauth/documentation/github.md +329 -0
  43. data/app/controllers/aven/oauth/documentation/google.md +253 -0
  44. data/app/controllers/aven/oauth/entra_id_controller.rb +92 -0
  45. data/app/controllers/aven/oauth/github_controller.rb +91 -0
  46. data/app/controllers/aven/oauth/google_controller.rb +64 -0
  47. data/app/controllers/aven/static_controller.rb +7 -0
  48. data/app/controllers/aven/tags_controller.rb +44 -0
  49. data/app/controllers/aven/workspaces_controller.rb +20 -0
  50. data/app/controllers/concerns/aven/authentication.rb +49 -0
  51. data/app/controllers/concerns/aven/controller_helpers.rb +38 -0
  52. data/app/helpers/aven/application_helper.rb +16 -0
  53. data/app/javascript/aven/application.js +3 -0
  54. data/app/javascript/aven/controllers/application.js +5 -0
  55. data/app/javascript/aven/controllers/index.js +11 -0
  56. data/app/jobs/aven/agentic/document_embedding_job.rb +28 -0
  57. data/app/jobs/aven/agentic/document_ocr_job.rb +28 -0
  58. data/app/jobs/aven/application_job.rb +4 -0
  59. data/app/jobs/aven/chat/calculate_cost_job.rb +26 -0
  60. data/app/jobs/aven/chat/run_job.rb +27 -0
  61. data/app/mailers/aven/application_mailer.rb +6 -0
  62. data/app/models/aven/agentic/agent.rb +76 -0
  63. data/app/models/aven/agentic/agent_document.rb +37 -0
  64. data/app/models/aven/agentic/agent_tool.rb +37 -0
  65. data/app/models/aven/agentic/document.rb +162 -0
  66. data/app/models/aven/agentic/document_embedding.rb +39 -0
  67. data/app/models/aven/agentic/tool.rb +106 -0
  68. data/app/models/aven/agentic/tool_parameter.rb +56 -0
  69. data/app/models/aven/application_record.rb +5 -0
  70. data/app/models/aven/article.rb +86 -0
  71. data/app/models/aven/article_attachment.rb +18 -0
  72. data/app/models/aven/article_relationship.rb +26 -0
  73. data/app/models/aven/chat/message.rb +135 -0
  74. data/app/models/aven/chat/thread.rb +159 -0
  75. data/app/models/aven/import/entry.rb +45 -0
  76. data/app/models/aven/import/item_link.rb +36 -0
  77. data/app/models/aven/import/processor.rb +123 -0
  78. data/app/models/aven/import.rb +102 -0
  79. data/app/models/aven/item/embed.rb +54 -0
  80. data/app/models/aven/item/embeddable.rb +141 -0
  81. data/app/models/aven/item/linkable.rb +212 -0
  82. data/app/models/aven/item/schema/builder.rb +139 -0
  83. data/app/models/aven/item/schemaed.rb +252 -0
  84. data/app/models/aven/item/schemas/base.rb +108 -0
  85. data/app/models/aven/item.rb +128 -0
  86. data/app/models/aven/item_link.rb +43 -0
  87. data/app/models/aven/item_schema.rb +87 -0
  88. data/app/models/aven/log.rb +66 -0
  89. data/app/models/aven/loggable.rb +20 -0
  90. data/app/models/aven/user.rb +40 -0
  91. data/app/models/aven/workspace.rb +93 -0
  92. data/app/models/aven/workspace_role.rb +46 -0
  93. data/app/models/aven/workspace_user.rb +54 -0
  94. data/app/models/aven/workspace_user_role.rb +38 -0
  95. data/app/models/concerns/aven/agentic/document_embeddable.rb +58 -0
  96. data/app/models/concerns/aven/searchable.rb +61 -0
  97. data/app/services/aven/agentic/dynamic_tool_builder.rb +81 -0
  98. data/app/services/aven/agentic/mcp/adapter.rb +77 -0
  99. data/app/services/aven/agentic/mcp/result_formatter.rb +57 -0
  100. data/app/services/aven/agentic/mcp/server_factory.rb +43 -0
  101. data/app/services/aven/agentic/ocr/base_extractor.rb +39 -0
  102. data/app/services/aven/agentic/ocr/excel_extractor.rb +43 -0
  103. data/app/services/aven/agentic/ocr/image_extractor.rb +22 -0
  104. data/app/services/aven/agentic/ocr/pdf_extractor.rb +48 -0
  105. data/app/services/aven/agentic/ocr/processor.rb +36 -0
  106. data/app/services/aven/agentic/ocr/textract_client.rb +131 -0
  107. data/app/services/aven/agentic/ocr/word_extractor.rb +34 -0
  108. data/app/services/aven/agentic/tool_result_formatter.rb +76 -0
  109. data/app/services/aven/agentic/tools/base.rb +55 -0
  110. data/app/services/aven/agentic/tools/concerns/boolean_filtering.rb +40 -0
  111. data/app/services/aven/agentic/tools/concerns/enum_filtering.rb +47 -0
  112. data/app/services/aven/agentic/tools/concerns/geo_filtering.rb +56 -0
  113. data/app/services/aven/agentic/tools/concerns/range_filtering.rb +51 -0
  114. data/app/services/aven/chat/broadcaster.rb +59 -0
  115. data/app/services/aven/chat/config.rb +93 -0
  116. data/app/services/aven/chat/message_builder.rb +42 -0
  117. data/app/services/aven/chat/orchestrator.rb +69 -0
  118. data/app/services/aven/chat/runner.rb +105 -0
  119. data/app/services/aven/chat/title_generator.rb +61 -0
  120. data/app/services/aven/external/gmail_client.rb +173 -0
  121. data/app/services/aven/external/google_contacts_client.rb +95 -0
  122. data/app/views/layouts/aven/admin.html.erb +16 -0
  123. data/app/views/layouts/aven/application.html.erb +18 -0
  124. data/config/importmap.rb +16 -0
  125. data/config/routes.rb +63 -0
  126. data/db/migrate/20200101000001_create_aven_users.rb +19 -0
  127. data/db/migrate/20200101000002_create_aven_workspaces.rb +14 -0
  128. data/db/migrate/20200101000003_create_aven_workspace_users.rb +12 -0
  129. data/db/migrate/20200101000004_create_aven_workspace_roles.rb +13 -0
  130. data/db/migrate/20200101000005_create_aven_workspace_user_roles.rb +12 -0
  131. data/db/migrate/20200101000006_create_aven_logs.rb +21 -0
  132. data/db/migrate/20200101000009_create_aven_items.rb +17 -0
  133. data/db/migrate/20200101000010_create_aven_item_links.rb +17 -0
  134. data/db/migrate/20200101000011_create_aven_agentic_tools.rb +19 -0
  135. data/db/migrate/20200101000012_create_aven_agentic_tool_parameters.rb +20 -0
  136. data/db/migrate/20200101000013_create_aven_agentic_documents.rb +22 -0
  137. data/db/migrate/20200101000014_create_aven_agentic_document_embeddings.rb +18 -0
  138. data/db/migrate/20200101000015_create_aven_agentic_agents.rb +18 -0
  139. data/db/migrate/20200101000016_create_aven_agentic_agent_tools.rb +13 -0
  140. data/db/migrate/20200101000017_create_aven_agentic_agent_documents.rb +13 -0
  141. data/db/migrate/20200101000018_create_aven_chat_threads.rb +19 -0
  142. data/db/migrate/20200101000019_create_aven_chat_messages.rb +26 -0
  143. data/db/migrate/20200101000020_add_pg_search_support.rb +21 -0
  144. data/db/migrate/20200101000021_create_aven_item_schemas.rb +18 -0
  145. data/db/migrate/20200101000022_create_aven_imports.rb +23 -0
  146. data/db/migrate/20200101000023_create_aven_import_entries.rb +13 -0
  147. data/db/migrate/20200101000024_create_aven_import_item_links.rb +13 -0
  148. data/db/migrate/20200101000025_create_aven_articles.rb +19 -0
  149. data/db/migrate/20200101000026_create_aven_article_attachments.rb +13 -0
  150. data/db/migrate/20200101000027_create_aven_article_relationships.rb +15 -0
  151. data/lib/aven/configuration.rb +87 -0
  152. data/lib/aven/engine.rb +43 -0
  153. data/lib/aven/model/tenant_model.rb +91 -0
  154. data/lib/aven/model.rb +6 -0
  155. data/lib/aven/version.rb +3 -0
  156. data/lib/aven.rb +8 -0
  157. data/lib/tasks/annotate_rb.rake +10 -0
  158. data/lib/tasks/aven_tasks.rake +21 -0
  159. metadata +426 -0
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ # == Schema Information
4
+ #
5
+ # Table name: aven_items
6
+ #
7
+ # id :bigint not null, primary key
8
+ # data :jsonb not null
9
+ # deleted_at :datetime
10
+ # schema_slug :string not null
11
+ # created_at :datetime not null
12
+ # updated_at :datetime not null
13
+ # workspace_id :bigint not null
14
+ #
15
+ # Indexes
16
+ #
17
+ # index_aven_items_on_data (data) USING gin
18
+ # index_aven_items_on_deleted_at (deleted_at)
19
+ # index_aven_items_on_schema_slug (schema_slug)
20
+ # index_aven_items_on_workspace_id (workspace_id)
21
+ #
22
+ # Foreign Keys
23
+ #
24
+ # fk_rails_... (workspace_id => aven_workspaces.id)
25
+ #
26
+ module Aven
27
+ class Item < ApplicationRecord
28
+ include Aven::Model::TenantModel
29
+ include Aven::Loggable
30
+ include Item::Schemaed
31
+ include Item::Embeddable
32
+ include Item::Linkable
33
+
34
+ self.table_name = "aven_items"
35
+
36
+ validates :schema_slug, presence: true
37
+ validates :data, presence: true
38
+ validate :validate_data_against_schema
39
+
40
+ scope :active, -> { where(deleted_at: nil) }
41
+ scope :deleted, -> { where.not(deleted_at: nil) }
42
+ scope :by_schema, ->(slug) { where(schema_slug: slug) }
43
+ scope :recent, -> { order(created_at: :desc) }
44
+
45
+ def soft_delete!
46
+ update!(deleted_at: Time.current)
47
+ end
48
+
49
+ def restore!
50
+ update!(deleted_at: nil)
51
+ end
52
+
53
+ def deleted?
54
+ deleted_at.present?
55
+ end
56
+
57
+ # Schema resolution: code class first, then DB, then raise
58
+ class << self
59
+ def schema_class_for(slug)
60
+ "Aven::Item::Schemas::#{slug.to_s.camelize}".constantize
61
+ rescue NameError
62
+ nil
63
+ end
64
+
65
+ def schema_for(slug, workspace: nil)
66
+ # 1. Try code-defined class
67
+ schema_class_for(slug) ||
68
+ # 2. Try DB (requires workspace)
69
+ (workspace && Aven::ItemSchema.find_by!(workspace:, slug:))
70
+ end
71
+ end
72
+
73
+ def schema_class
74
+ self.class.schema_class_for(schema_slug)
75
+ end
76
+
77
+ # Returns the schema source (code class or DB record)
78
+ def resolved_schema
79
+ @_resolved_schema ||= begin
80
+ # 1. Code class first
81
+ code_schema = self.class.schema_class_for(schema_slug)
82
+ return code_schema if code_schema
83
+
84
+ # 2. DB lookup (raises if not found)
85
+ Aven::ItemSchema.find_by!(workspace:, slug: schema_slug)
86
+ end
87
+ end
88
+
89
+ def schema_builder
90
+ resolved_schema&.builder
91
+ end
92
+
93
+ private
94
+
95
+ def validate_data_against_schema
96
+ return if schema_slug.blank? || data.blank?
97
+
98
+ begin
99
+ json_schema = resolved_schema&.to_json_schema
100
+ return if json_schema.blank?
101
+
102
+ registry = JSONSkooma.create_registry("2020-12", assert_formats: true)
103
+ schema_with_meta = json_schema.dup
104
+ schema_with_meta["$schema"] ||= "https://json-schema.org/draft/2020-12/schema"
105
+ validator = JSONSkooma::JSONSchema.new(schema_with_meta, registry:)
106
+ result = validator.evaluate(data)
107
+
108
+ unless result.valid?
109
+ error_output = result.output(:basic)
110
+ if error_output["errors"]
111
+ error_messages = error_output["errors"].map do |err|
112
+ location = err["instanceLocation"] || "data"
113
+ message = err["error"] || "validation failed"
114
+ "#{location}: #{message}"
115
+ end
116
+ errors.add(:data, "schema validation failed: #{error_messages.join('; ')}")
117
+ else
118
+ errors.add(:data, "does not conform to schema")
119
+ end
120
+ end
121
+ rescue ActiveRecord::RecordNotFound
122
+ errors.add(:schema_slug, "schema '#{schema_slug}' not found")
123
+ rescue => e
124
+ errors.add(:data, "schema validation error: #{e.message}")
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # == Schema Information
4
+ #
5
+ # Table name: aven_item_links
6
+ #
7
+ # id :bigint not null, primary key
8
+ # position :integer default(0)
9
+ # relation :string not null
10
+ # created_at :datetime not null
11
+ # updated_at :datetime not null
12
+ # source_id :bigint not null
13
+ # target_id :bigint not null
14
+ #
15
+ # Indexes
16
+ #
17
+ # index_aven_item_links_on_source_id (source_id)
18
+ # index_aven_item_links_on_source_id_and_relation (source_id,relation)
19
+ # index_aven_item_links_on_source_id_and_target_id_and_relation (source_id,target_id,relation) UNIQUE
20
+ # index_aven_item_links_on_target_id (target_id)
21
+ # index_aven_item_links_on_target_id_and_relation (target_id,relation)
22
+ #
23
+ # Foreign Keys
24
+ #
25
+ # fk_rails_... (source_id => aven_items.id)
26
+ # fk_rails_... (target_id => aven_items.id)
27
+ #
28
+ module Aven
29
+ class ItemLink < ApplicationRecord
30
+ self.table_name = "aven_item_links"
31
+
32
+ belongs_to :source, class_name: "Aven::Item"
33
+ belongs_to :target, class_name: "Aven::Item"
34
+
35
+ validates :relation, presence: true
36
+ validates :target_id, uniqueness: { scope: [:source_id, :relation] }
37
+
38
+ scope :for_relation, ->(rel) { where(relation: rel.to_s) }
39
+ scope :ordered, -> { order(position: :asc) }
40
+
41
+ delegate :workspace, :workspace_id, to: :source
42
+ end
43
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ # == Schema Information
4
+ #
5
+ # Table name: aven_item_schemas
6
+ #
7
+ # id :bigint not null, primary key
8
+ # embeds :jsonb not null
9
+ # fields :jsonb not null
10
+ # links :jsonb not null
11
+ # schema :jsonb not null
12
+ # slug :string not null
13
+ # created_at :datetime not null
14
+ # updated_at :datetime not null
15
+ # workspace_id :bigint not null
16
+ #
17
+ # Indexes
18
+ #
19
+ # index_aven_item_schemas_on_slug (slug)
20
+ # index_aven_item_schemas_on_workspace_id (workspace_id)
21
+ # index_aven_item_schemas_on_workspace_id_and_slug (workspace_id,slug) UNIQUE
22
+ #
23
+ # Foreign Keys
24
+ #
25
+ # fk_rails_... (workspace_id => aven_workspaces.id)
26
+ #
27
+ module Aven
28
+ class ItemSchema < ApplicationRecord
29
+ self.table_name = "aven_item_schemas"
30
+
31
+ include Aven::Model::TenantModel
32
+ include Aven::Loggable
33
+
34
+ has_many :items, ->(schema) { where(schema_slug: schema.slug) },
35
+ class_name: "Aven::Item",
36
+ foreign_key: false,
37
+ inverse_of: false
38
+
39
+ validates :slug, presence: true,
40
+ uniqueness: { scope: :workspace_id },
41
+ format: { with: /\A[a-z][a-z0-9_]*\z/, message: "must be lowercase, start with letter, contain only letters/numbers/underscores" }
42
+ validates :schema, presence: true
43
+ validate :validate_schema_format
44
+
45
+ # Mimic the interface of code-defined schemas (Item::Schemas::Base)
46
+ def builder
47
+ self
48
+ end
49
+
50
+ def to_json_schema
51
+ schema
52
+ end
53
+
54
+ # Accessors that match Item::Schemas::Base interface
55
+ def fields_config
56
+ (fields || {}).deep_symbolize_keys
57
+ end
58
+
59
+ def embeds_config
60
+ (embeds || {}).deep_symbolize_keys
61
+ end
62
+
63
+ def links_config
64
+ (links || {}).deep_symbolize_keys
65
+ end
66
+
67
+ # Alias methods to match Schemas::Base
68
+ alias_method :schema_fields, :fields_config
69
+ alias_method :schema_embeds, :embeds_config
70
+ alias_method :schema_links, :links_config
71
+
72
+ private
73
+
74
+ def validate_schema_format
75
+ return if schema.blank?
76
+
77
+ unless schema.is_a?(Hash)
78
+ errors.add(:schema, "must be a valid JSON object")
79
+ return
80
+ end
81
+
82
+ unless schema["type"].present?
83
+ errors.add(:schema, "must include a 'type' property")
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,66 @@
1
+ # == Schema Information
2
+ #
3
+ # Table name: aven_logs
4
+ #
5
+ # id :bigint not null, primary key
6
+ # level :string default("info"), not null
7
+ # loggable_type :string not null
8
+ # message :text not null
9
+ # metadata :jsonb
10
+ # state :string
11
+ # state_machine :string
12
+ # created_at :datetime not null
13
+ # updated_at :datetime not null
14
+ # loggable_id :bigint not null
15
+ # run_id :string
16
+ # workspace_id :bigint not null
17
+ #
18
+ # Indexes
19
+ #
20
+ # idx_aven_logs_on_loggable_run_state_created_at (loggable_type,loggable_id,run_id,state,created_at)
21
+ # index_aven_logs_on_created_at (created_at)
22
+ # index_aven_logs_on_level (level)
23
+ # index_aven_logs_on_loggable (loggable_type,loggable_id)
24
+ # index_aven_logs_on_workspace_id (workspace_id)
25
+ #
26
+ # Foreign Keys
27
+ #
28
+ # fk_rails_... (workspace_id => aven_workspaces.id)
29
+ #
30
+ module Aven
31
+ class Log < ApplicationRecord
32
+ self.table_name = "aven_logs"
33
+
34
+ LEVELS = %w[debug info warn error fatal].freeze
35
+
36
+ belongs_to :loggable, polymorphic: true
37
+ belongs_to :workspace, class_name: "Aven::Workspace"
38
+
39
+ validates :message, presence: true
40
+ validates :level, inclusion: { in: LEVELS }
41
+
42
+ scope :by_level, ->(level) { where(level:) }
43
+ scope :recent, -> { order(created_at: :desc) }
44
+
45
+ before_validation :apply_loggable_context
46
+
47
+ private
48
+
49
+ def apply_loggable_context
50
+ owner = loggable
51
+ return unless owner
52
+
53
+ if respond_to?(:workspace_id) && workspace_id.blank?
54
+ if owner.is_a?(Aven::Workspace)
55
+ self.workspace = owner
56
+ elsif owner.respond_to?(:workspace)
57
+ self.workspace = owner.workspace
58
+ end
59
+ end
60
+
61
+ if respond_to?(:run_id) && run_id.blank? && owner.respond_to?(:_log_run_id)
62
+ self.run_id = owner._log_run_id
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,20 @@
1
+ module Aven
2
+ module Loggable
3
+ def log!(message:, level: "info", metadata: nil, **extra)
4
+ attrs = {
5
+ message:,
6
+ level:,
7
+ metadata: metadata || {}
8
+ }
9
+
10
+ ws = is_a?(Aven::Workspace) ? self : workspace
11
+ attrs[:workspace] = ws
12
+
13
+ attrs[:run_id] = extra[:run_id] if extra.key?(:run_id)
14
+ attrs[:state] = extra[:state] if extra.key?(:state)
15
+ attrs[:state_machine] = extra[:state_machine] if extra.key?(:state_machine)
16
+
17
+ logs.create!(attrs)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,40 @@
1
+ # == Schema Information
2
+ #
3
+ # Table name: aven_users
4
+ #
5
+ # id :bigint not null, primary key
6
+ # access_token :string
7
+ # admin :boolean default(FALSE), not null
8
+ # auth_tenant :string
9
+ # email :string default(""), not null
10
+ # encrypted_password :string default(""), not null
11
+ # remember_created_at :datetime
12
+ # reset_password_sent_at :datetime
13
+ # reset_password_token :string
14
+ # created_at :datetime not null
15
+ # updated_at :datetime not null
16
+ # remote_id :string
17
+ #
18
+ # Indexes
19
+ #
20
+ # index_aven_users_on_email_and_auth_tenant (email,auth_tenant) UNIQUE
21
+ # index_aven_users_on_reset_password_token (reset_password_token) UNIQUE
22
+ #
23
+ module Aven
24
+ class User < ApplicationRecord
25
+ has_many :workspace_users, dependent: :destroy
26
+ has_many :workspaces, through: :workspace_users
27
+ has_many :workspace_user_roles, through: :workspace_users
28
+ has_many :workspace_roles, through: :workspace_user_roles
29
+
30
+ # has_many(:repos, class_name: UserRepo.name)
31
+ # has_many(:deployments)
32
+ # has_many(:strategies, foreign_key: :author_id, inverse_of: :author)
33
+ # has_many(:credentials, class_name: ProviderCredential.name)
34
+
35
+ validates :email, presence: true, uniqueness: { scope: :auth_tenant, case_sensitive: false }, format: { with: URI::MailTo::EMAIL_REGEXP }
36
+ validates :remote_id, uniqueness: { scope: :auth_tenant, case_sensitive: false }, allow_blank: true
37
+
38
+ encrypts(:access_token)
39
+ end
40
+ end
@@ -0,0 +1,93 @@
1
+ # == Schema Information
2
+ #
3
+ # Table name: aven_workspaces
4
+ #
5
+ # id :bigint not null, primary key
6
+ # description :text
7
+ # domain :string
8
+ # label :string
9
+ # slug :string
10
+ # created_at :datetime not null
11
+ # updated_at :datetime not null
12
+ #
13
+ # Indexes
14
+ #
15
+ # index_aven_workspaces_on_slug (slug) UNIQUE
16
+ #
17
+ module Aven
18
+ class Workspace < ApplicationRecord
19
+ extend FriendlyId
20
+ friendly_id :label, use: :slugged
21
+
22
+ self.table_name = "aven_workspaces"
23
+
24
+ has_many :workspace_users, class_name: "Aven::WorkspaceUser", dependent: :destroy
25
+ has_many :users, through: :workspace_users, class_name: "Aven::User"
26
+ has_many :workspace_roles, class_name: "Aven::WorkspaceRole", dependent: :destroy
27
+ has_many :workspace_user_roles, through: :workspace_roles, class_name: "Aven::WorkspaceUserRole"
28
+
29
+ # Agentic associations
30
+ has_many :aven_agentic_agents, class_name: "Aven::Agentic::Agent", dependent: :destroy
31
+ has_many :aven_agentic_documents, class_name: "Aven::Agentic::Document", dependent: :destroy
32
+
33
+ # Chat associations
34
+ has_many :aven_chat_threads, class_name: "Aven::Chat::Thread", dependent: :destroy
35
+
36
+ # Article associations
37
+ has_many :aven_articles, class_name: "Aven::Article", dependent: :destroy
38
+
39
+ validates :slug, uniqueness: true, allow_blank: true
40
+ validates :label, length: { maximum: 255 }, allow_blank: true
41
+ validates :description, length: { maximum: 1000 }, allow_blank: true
42
+
43
+ # Tenant model registry (inspired by Flipper's group registry pattern)
44
+ class << self
45
+ # Returns array of all registered tenant model classes
46
+ def tenant_models
47
+ @tenant_models ||= []
48
+ end
49
+
50
+ # Register a model class as workspace-scoped
51
+ # Called automatically when a model includes Aven::TenantModel
52
+ def register_tenant_model(model_class)
53
+ return if tenant_models.include?(model_class)
54
+
55
+ tenant_models << model_class
56
+ define_tenant_association(model_class)
57
+ end
58
+
59
+ # Get all registered tenant model class names
60
+ def tenant_model_names
61
+ tenant_models.map(&:name)
62
+ end
63
+
64
+ private
65
+
66
+ # Define association method for a tenant model
67
+ # Creates query method that returns ActiveRecord::Relation
68
+ def define_tenant_association(model_class)
69
+ association_name = model_class.workspace_association_name
70
+
71
+ # Define instance method for querying tenant records
72
+ define_method(association_name) do
73
+ model_class.where(workspace_id: id)
74
+ end
75
+ end
76
+ end
77
+
78
+ # Find a tenant record by type and ID
79
+ def find_tenant_record(model_name, record_id)
80
+ model_class = self.class.tenant_models.find { |m| m.name == model_name }
81
+ return nil unless model_class
82
+
83
+ model_class.where(workspace_id: id).find(record_id)
84
+ end
85
+
86
+ # Destroy all tenant data for this workspace
87
+ def destroy_tenant_data
88
+ self.class.tenant_models.each do |model_class|
89
+ model_class.where(workspace_id: id).destroy_all
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,46 @@
1
+ # == Schema Information
2
+ #
3
+ # Table name: aven_workspace_roles
4
+ #
5
+ # id :bigint not null, primary key
6
+ # description :string
7
+ # label :string not null
8
+ # created_at :datetime not null
9
+ # updated_at :datetime not null
10
+ # workspace_id :bigint
11
+ #
12
+ # Indexes
13
+ #
14
+ # idx_aven_workspace_roles_on_ws_label (workspace_id,label) UNIQUE
15
+ # index_aven_workspace_roles_on_workspace_id (workspace_id)
16
+ #
17
+ # Foreign Keys
18
+ #
19
+ # fk_rails_... (workspace_id => aven_workspaces.id)
20
+ #
21
+ module Aven
22
+ class WorkspaceRole < ApplicationRecord
23
+ self.table_name = "aven_workspace_roles"
24
+
25
+ belongs_to :workspace, class_name: "Aven::Workspace"
26
+ has_many :workspace_user_roles, class_name: "Aven::WorkspaceUserRole", dependent: :destroy
27
+ has_many :workspace_users, through: :workspace_user_roles, class_name: "Aven::WorkspaceUser"
28
+ has_many :users, through: :workspace_users, class_name: "Aven::User"
29
+
30
+ validates :label, presence: true
31
+ validates :label, uniqueness: { scope: :workspace_id }
32
+
33
+ PREDEFINED_ROLES = %w[owner admin member viewer].freeze
34
+
35
+ scope :predefined, -> { where(label: PREDEFINED_ROLES) }
36
+ scope :custom, -> { where.not(label: PREDEFINED_ROLES) }
37
+
38
+ def predefined?
39
+ PREDEFINED_ROLES.include?(label)
40
+ end
41
+
42
+ def custom?
43
+ !predefined?
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,54 @@
1
+ # == Schema Information
2
+ #
3
+ # Table name: aven_workspace_users
4
+ #
5
+ # id :bigint not null, primary key
6
+ # created_at :datetime not null
7
+ # updated_at :datetime not null
8
+ # user_id :bigint not null
9
+ # workspace_id :bigint not null
10
+ #
11
+ # Indexes
12
+ #
13
+ # idx_aven_workspace_users_on_user_workspace (user_id,workspace_id) UNIQUE
14
+ # index_aven_workspace_users_on_user_id (user_id)
15
+ # index_aven_workspace_users_on_workspace_id (workspace_id)
16
+ #
17
+ # Foreign Keys
18
+ #
19
+ # fk_rails_... (user_id => aven_users.id)
20
+ # fk_rails_... (workspace_id => aven_workspaces.id)
21
+ #
22
+ module Aven
23
+ class WorkspaceUser < ApplicationRecord
24
+ self.table_name = "aven_workspace_users"
25
+
26
+ belongs_to :user, class_name: "Aven::User"
27
+ belongs_to :workspace, class_name: "Aven::Workspace"
28
+
29
+ has_many :workspace_user_roles, class_name: "Aven::WorkspaceUserRole", dependent: :destroy
30
+ has_many :workspace_roles, through: :workspace_user_roles, class_name: "Aven::WorkspaceRole"
31
+
32
+ validates :user_id, uniqueness: { scope: :workspace_id }
33
+
34
+ def roles
35
+ workspace_roles.pluck(:label)
36
+ end
37
+
38
+ def has_role?(role_label)
39
+ workspace_roles.exists?(label: role_label)
40
+ end
41
+
42
+ def add_role(role_label)
43
+ role = workspace.workspace_roles.find_or_create_by!(label: role_label)
44
+ workspace_user_roles.find_or_create_by!(workspace_role: role)
45
+ end
46
+
47
+ def remove_role(role_label)
48
+ role = workspace.workspace_roles.find_by(label: role_label)
49
+ return unless role
50
+
51
+ workspace_user_roles.where(workspace_role: role).destroy_all
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,38 @@
1
+ # == Schema Information
2
+ #
3
+ # Table name: aven_workspace_user_roles
4
+ #
5
+ # id :bigint not null, primary key
6
+ # created_at :datetime not null
7
+ # updated_at :datetime not null
8
+ # workspace_role_id :bigint
9
+ # workspace_user_id :bigint
10
+ #
11
+ # Indexes
12
+ #
13
+ # idx_aven_ws_user_roles_on_role_user (workspace_role_id,workspace_user_id) UNIQUE
14
+ # index_aven_workspace_user_roles_on_workspace_role_id (workspace_role_id)
15
+ # index_aven_workspace_user_roles_on_workspace_user_id (workspace_user_id)
16
+ #
17
+ # Foreign Keys
18
+ #
19
+ # fk_rails_... (workspace_role_id => aven_workspace_roles.id)
20
+ # fk_rails_... (workspace_user_id => aven_workspace_users.id)
21
+ #
22
+ module Aven
23
+ class WorkspaceUserRole < ApplicationRecord
24
+ self.table_name = "aven_workspace_user_roles"
25
+
26
+ belongs_to :workspace_user, class_name: "Aven::WorkspaceUser"
27
+ belongs_to :workspace_role, class_name: "Aven::WorkspaceRole"
28
+
29
+ validates :workspace_user_id, uniqueness: { scope: :workspace_role_id }
30
+
31
+ delegate :workspace, :label, :description, to: :workspace_role
32
+ delegate :user, to: :workspace_user
33
+ delegate :email, :username, to: :user
34
+
35
+ scope :for_workspace, ->(workspace) { joins(:workspace_role).where(aven_workspace_roles: { workspace_id: workspace.id }) }
36
+ scope :with_role, ->(role_label) { joins(:workspace_role).where(aven_workspace_roles: { label: role_label }) }
37
+ end
38
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aven
4
+ module Agentic
5
+ module DocumentEmbeddable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ # Override in subclasses to customize embedding behavior
10
+ end
11
+
12
+ # Split content into chunks for embedding
13
+ def chunk_content(content, chunk_size: 1000, overlap: 200)
14
+ return [] if content.blank?
15
+
16
+ chunks = []
17
+ position = 0
18
+
19
+ while position < content.length
20
+ chunk_end = [position + chunk_size, content.length].min
21
+ chunks << content[position...chunk_end]
22
+ position += (chunk_size - overlap)
23
+ end
24
+
25
+ chunks
26
+ end
27
+
28
+ # Generate embeddings for all chunks
29
+ def generate_embeddings!
30
+ return unless ocr_content.present?
31
+
32
+ chunks = chunk_content(ocr_content)
33
+
34
+ transaction do
35
+ embeddings.destroy_all
36
+
37
+ chunks.each_with_index do |chunk, index|
38
+ embeddings.create!(
39
+ chunk_index: index,
40
+ content: chunk,
41
+ embedding: nil # Will be filled by embedding service
42
+ )
43
+ end
44
+ end
45
+ end
46
+
47
+ # Search similar chunks using vector similarity
48
+ def self.search_similar(query_embedding, workspace:, limit: 10)
49
+ Aven::Agentic::DocumentEmbedding
50
+ .joins(:document)
51
+ .where(aven_agentic_documents: { workspace_id: workspace.id })
52
+ .where.not(embedding: nil)
53
+ .order(Arel.sql("embedding <-> '#{query_embedding}'"))
54
+ .limit(limit)
55
+ end
56
+ end
57
+ end
58
+ end