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,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ # == Schema Information
4
+ #
5
+ # Table name: aven_agentic_documents
6
+ #
7
+ # id :bigint not null, primary key
8
+ # byte_size :bigint not null
9
+ # content_type :string not null
10
+ # embedding_status :string default("pending"), not null
11
+ # filename :string not null
12
+ # metadata :jsonb
13
+ # ocr_content :text
14
+ # ocr_status :string default("pending"), not null
15
+ # processed_at :datetime
16
+ # created_at :datetime not null
17
+ # updated_at :datetime not null
18
+ # workspace_id :bigint not null
19
+ #
20
+ # Indexes
21
+ #
22
+ # index_aven_agentic_documents_on_content_type (content_type)
23
+ # index_aven_agentic_documents_on_embedding_status (embedding_status)
24
+ # index_aven_agentic_documents_on_ocr_status (ocr_status)
25
+ # index_aven_agentic_documents_on_workspace_id (workspace_id)
26
+ #
27
+ # Foreign Keys
28
+ #
29
+ # fk_rails_... (workspace_id => aven_workspaces.id)
30
+ #
31
+ module Aven
32
+ module Agentic
33
+ class Document < Aven::ApplicationRecord
34
+ self.table_name = "aven_agentic_documents"
35
+
36
+ include Aven::Model::TenantModel
37
+ include Aven::Agentic::DocumentEmbeddable
38
+
39
+ has_one_attached :file
40
+
41
+ has_many :embeddings,
42
+ class_name: "Aven::Agentic::DocumentEmbedding",
43
+ foreign_key: :document_id,
44
+ dependent: :destroy
45
+
46
+ has_many :agent_documents,
47
+ class_name: "Aven::Agentic::AgentDocument",
48
+ foreign_key: :document_id,
49
+ dependent: :destroy,
50
+ inverse_of: :document
51
+
52
+ has_many :agents, through: :agent_documents
53
+
54
+ # Statuses
55
+ OCR_STATUSES = %w[pending processing completed failed skipped].freeze
56
+ EMBEDDING_STATUSES = %w[pending processing completed failed].freeze
57
+
58
+ # Allowed file types
59
+ ALLOWED_CONTENT_TYPES = [
60
+ "application/pdf",
61
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
62
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
63
+ "image/jpeg",
64
+ "image/png",
65
+ "image/gif",
66
+ "image/webp"
67
+ ].freeze
68
+
69
+ MAX_FILE_SIZE = 30.megabytes
70
+
71
+ validates :filename, presence: true
72
+ validates :content_type, presence: true, inclusion: { in: ALLOWED_CONTENT_TYPES }
73
+ validates :byte_size, presence: true, numericality: { less_than_or_equal_to: MAX_FILE_SIZE }
74
+ validates :ocr_status, inclusion: { in: OCR_STATUSES }
75
+ validates :embedding_status, inclusion: { in: EMBEDDING_STATUSES }
76
+ validate :file_attached
77
+
78
+ after_create_commit :enqueue_ocr_job
79
+
80
+ scope :recent, -> { order(created_at: :desc) }
81
+ scope :by_type, ->(type) { where("content_type LIKE ?", "#{type}%") }
82
+ scope :images, -> { where("content_type LIKE ?", "image/%") }
83
+ scope :pdfs, -> { where(content_type: "application/pdf") }
84
+ scope :with_ocr, -> { where(ocr_status: "completed").where.not(ocr_content: nil) }
85
+ scope :pending_ocr, -> { where(ocr_status: "pending") }
86
+ scope :pending_embedding, -> { where(embedding_status: "pending", ocr_status: "completed") }
87
+
88
+ def image?
89
+ content_type.start_with?("image/")
90
+ end
91
+
92
+ def pdf?
93
+ content_type == "application/pdf"
94
+ end
95
+
96
+ def word_doc?
97
+ content_type.include?("wordprocessingml")
98
+ end
99
+
100
+ def excel?
101
+ content_type.include?("spreadsheetml")
102
+ end
103
+
104
+ def ocr_required?
105
+ image? || pdf?
106
+ end
107
+
108
+ def mark_ocr_processing!
109
+ update!(ocr_status: "processing")
110
+ end
111
+
112
+ def mark_ocr_completed!(content)
113
+ update!(
114
+ ocr_status: "completed",
115
+ ocr_content: content,
116
+ processed_at: Time.current
117
+ )
118
+ enqueue_embedding_job if content.present?
119
+ end
120
+
121
+ def mark_ocr_failed!(error = nil)
122
+ update!(
123
+ ocr_status: "failed",
124
+ metadata: metadata.merge("ocr_error" => error)
125
+ )
126
+ end
127
+
128
+ def mark_ocr_skipped!
129
+ update!(ocr_status: "skipped")
130
+ end
131
+
132
+ def mark_embedding_processing!
133
+ update!(embedding_status: "processing")
134
+ end
135
+
136
+ def mark_embedding_completed!
137
+ update!(embedding_status: "completed")
138
+ end
139
+
140
+ def mark_embedding_failed!(error = nil)
141
+ update!(
142
+ embedding_status: "failed",
143
+ metadata: metadata.merge("embedding_error" => error)
144
+ )
145
+ end
146
+
147
+ private
148
+
149
+ def file_attached
150
+ errors.add(:file, "must be attached") unless file.attached?
151
+ end
152
+
153
+ def enqueue_ocr_job
154
+ Aven::Agentic::DocumentOcrJob.perform_later(id) if ocr_required?
155
+ end
156
+
157
+ def enqueue_embedding_job
158
+ Aven::Agentic::DocumentEmbeddingJob.perform_later(id)
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # == Schema Information
4
+ #
5
+ # Table name: aven_agentic_document_embeddings
6
+ #
7
+ # id :bigint not null, primary key
8
+ # chunk_index :integer not null
9
+ # content :text not null
10
+ # embedding :vector(1536)
11
+ # created_at :datetime not null
12
+ # updated_at :datetime not null
13
+ # document_id :bigint not null
14
+ #
15
+ # Indexes
16
+ #
17
+ # idx_on_document_id_chunk_index_5fe199c056 (document_id,chunk_index) UNIQUE
18
+ # index_aven_agentic_document_embeddings_on_document_id (document_id)
19
+ #
20
+ # Foreign Keys
21
+ #
22
+ # fk_rails_... (document_id => aven_agentic_documents.id)
23
+ #
24
+ module Aven
25
+ module Agentic
26
+ class DocumentEmbedding < Aven::ApplicationRecord
27
+ self.table_name = "aven_agentic_document_embeddings"
28
+
29
+ belongs_to :document, class_name: "Aven::Agentic::Document"
30
+
31
+ validates :chunk_index, presence: true, uniqueness: { scope: :document_id }
32
+ validates :content, presence: true
33
+
34
+ scope :ordered, -> { order(:chunk_index) }
35
+
36
+ delegate :workspace, :workspace_id, to: :document
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ # == Schema Information
4
+ #
5
+ # Table name: aven_agentic_tools
6
+ #
7
+ # id :bigint not null, primary key
8
+ # class_name :string not null
9
+ # default_description :text
10
+ # description :text
11
+ # enabled :boolean default(TRUE), not null
12
+ # name :string not null
13
+ # created_at :datetime not null
14
+ # updated_at :datetime not null
15
+ # workspace_id :bigint
16
+ #
17
+ # Indexes
18
+ #
19
+ # index_aven_agentic_tools_on_enabled (enabled)
20
+ # index_aven_agentic_tools_on_workspace_id (workspace_id)
21
+ # index_aven_agentic_tools_on_workspace_id_and_class_name (workspace_id,class_name) UNIQUE
22
+ # index_aven_agentic_tools_on_workspace_id_and_name (workspace_id,name) UNIQUE
23
+ #
24
+ # Foreign Keys
25
+ #
26
+ # fk_rails_... (workspace_id => aven_workspaces.id)
27
+ #
28
+ module Aven
29
+ module Agentic
30
+ class Tool < Aven::ApplicationRecord
31
+ self.table_name = "aven_agentic_tools"
32
+
33
+ belongs_to :workspace, class_name: "Aven::Workspace", optional: true
34
+
35
+ has_many :parameters,
36
+ class_name: "Aven::Agentic::ToolParameter",
37
+ foreign_key: :tool_id,
38
+ dependent: :destroy,
39
+ inverse_of: :tool
40
+
41
+ has_many :agent_tools,
42
+ class_name: "Aven::Agentic::AgentTool",
43
+ foreign_key: :tool_id,
44
+ dependent: :destroy,
45
+ inverse_of: :tool
46
+
47
+ has_many :agents, through: :agent_tools
48
+
49
+ accepts_nested_attributes_for :parameters, allow_destroy: true
50
+
51
+ validates :name, presence: true, uniqueness: { scope: :workspace_id }
52
+ validates :class_name, presence: true, uniqueness: { scope: :workspace_id }
53
+
54
+ scope :enabled, -> { where(enabled: true) }
55
+ scope :global, -> { where(workspace_id: nil) }
56
+ scope :for_workspace, ->(workspace) {
57
+ where(workspace_id: [nil, workspace.id])
58
+ }
59
+
60
+ # Get the actual tool class
61
+ def tool_class
62
+ class_name.constantize
63
+ rescue NameError
64
+ nil
65
+ end
66
+
67
+ # Check if tool class exists and is valid
68
+ def valid_class?
69
+ klass = tool_class
70
+ klass.present? && klass < Aven::Agentic::Tools::Base
71
+ end
72
+
73
+ # Get effective description (user-enriched or default)
74
+ def effective_description
75
+ description.presence || default_description
76
+ end
77
+
78
+ # Sync from code definition
79
+ def sync_from_code!
80
+ klass = tool_class
81
+ return false unless klass
82
+
83
+ self.default_description = klass.default_description
84
+
85
+ code_params = klass.parameters
86
+ code_names = code_params.map { |p| p.name.to_s }
87
+
88
+ # Remove deleted parameters
89
+ parameters.where.not(name: code_names).destroy_all
90
+
91
+ # Create or update parameters
92
+ code_params.each_with_index do |param_def, index|
93
+ param = parameters.find_or_initialize_by(name: param_def.name.to_s)
94
+ param.param_type = param_def.type.to_s
95
+ param.default_description = param_def.description
96
+ param.required = param_def.required || false
97
+ param.constraints = param_def.constraints || {}
98
+ param.position = index
99
+ param.save!
100
+ end
101
+
102
+ save!
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # == Schema Information
4
+ #
5
+ # Table name: aven_agentic_tool_parameters
6
+ #
7
+ # id :bigint not null, primary key
8
+ # constraints :jsonb
9
+ # default_description :text
10
+ # description :text
11
+ # name :string not null
12
+ # param_type :string not null
13
+ # position :integer default(0), not null
14
+ # required :boolean default(FALSE), not null
15
+ # created_at :datetime not null
16
+ # updated_at :datetime not null
17
+ # tool_id :bigint not null
18
+ #
19
+ # Indexes
20
+ #
21
+ # index_aven_agentic_tool_parameters_on_tool_id (tool_id)
22
+ # index_aven_agentic_tool_parameters_on_tool_id_and_name (tool_id,name) UNIQUE
23
+ # index_aven_agentic_tool_parameters_on_tool_id_and_position (tool_id,position)
24
+ #
25
+ # Foreign Keys
26
+ #
27
+ # fk_rails_... (tool_id => aven_agentic_tools.id)
28
+ #
29
+ module Aven
30
+ module Agentic
31
+ class ToolParameter < Aven::ApplicationRecord
32
+ self.table_name = "aven_agentic_tool_parameters"
33
+
34
+ belongs_to :tool, class_name: "Aven::Agentic::Tool", inverse_of: :parameters
35
+
36
+ validates :name, presence: true, uniqueness: { scope: :tool_id }
37
+ validates :param_type, presence: true
38
+
39
+ default_scope { order(:position) }
40
+
41
+ PARAM_TYPES = %w[string integer float boolean array object].freeze
42
+
43
+ validates :param_type, inclusion: { in: PARAM_TYPES }
44
+
45
+ # Get effective description (user-enriched or default)
46
+ def effective_description
47
+ description.presence || default_description
48
+ end
49
+
50
+ # Check if parameter is required
51
+ def required?
52
+ required
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,5 @@
1
+ module Aven
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aven
4
+ class Article < ApplicationRecord
5
+ self.table_name = "aven_articles"
6
+
7
+ include Aven::Model::TenantModel
8
+
9
+ extend FriendlyId
10
+ friendly_id :title, use: [:slugged, :scoped], scope: :workspace
11
+
12
+ acts_as_taggable_on :tags
13
+
14
+ # Associations
15
+ belongs_to :author, class_name: "Aven::User", optional: true
16
+
17
+ has_one_attached :main_visual
18
+
19
+ has_many :article_attachments,
20
+ class_name: "Aven::ArticleAttachment",
21
+ foreign_key: :article_id,
22
+ dependent: :destroy,
23
+ inverse_of: :article
24
+
25
+ has_many :article_relationships,
26
+ -> { order(position: :asc) },
27
+ class_name: "Aven::ArticleRelationship",
28
+ foreign_key: :article_id,
29
+ dependent: :destroy,
30
+ inverse_of: :article
31
+
32
+ has_many :related_articles,
33
+ through: :article_relationships,
34
+ source: :related_article
35
+
36
+ has_many :inverse_article_relationships,
37
+ class_name: "Aven::ArticleRelationship",
38
+ foreign_key: :related_article_id,
39
+ dependent: :destroy
40
+
41
+ has_many :inverse_related_articles,
42
+ through: :inverse_article_relationships,
43
+ source: :article
44
+
45
+ # Nested attributes
46
+ accepts_nested_attributes_for :article_attachments, allow_destroy: true
47
+ accepts_nested_attributes_for :article_relationships, allow_destroy: true
48
+
49
+ # Validations
50
+ validates :title, presence: true
51
+ validates :slug, uniqueness: { scope: :workspace_id }, allow_blank: true
52
+
53
+ # Scopes
54
+ scope :published, -> { where.not(published_at: nil).where("published_at <= ?", Time.current) }
55
+ scope :draft, -> { where(published_at: nil) }
56
+ scope :scheduled, -> { where.not(published_at: nil).where("published_at > ?", Time.current) }
57
+ scope :recent, -> { order(Arel.sql("published_at DESC NULLS LAST, created_at DESC")) }
58
+ scope :by_author, ->(user) { where(author: user) }
59
+
60
+ # Instance methods
61
+ def published?
62
+ published_at.present? && published_at <= Time.current
63
+ end
64
+
65
+ def draft?
66
+ published_at.nil?
67
+ end
68
+
69
+ def scheduled?
70
+ published_at.present? && published_at > Time.current
71
+ end
72
+
73
+ def publish!
74
+ update!(published_at: Time.current)
75
+ end
76
+
77
+ def unpublish!
78
+ update!(published_at: nil)
79
+ end
80
+
81
+ # Returns sorted attachments
82
+ def sorted_attachments
83
+ article_attachments.order(position: :asc)
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aven
4
+ class ArticleAttachment < ApplicationRecord
5
+ self.table_name = "aven_article_attachments"
6
+
7
+ belongs_to :article, class_name: "Aven::Article", inverse_of: :article_attachments
8
+
9
+ has_one_attached :file
10
+
11
+ validates :position, presence: true, numericality: { greater_than_or_equal_to: 0 }
12
+
13
+ scope :ordered, -> { order(position: :asc) }
14
+
15
+ # Delegate workspace access
16
+ delegate :workspace, :workspace_id, to: :article
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aven
4
+ class ArticleRelationship < ApplicationRecord
5
+ self.table_name = "aven_article_relationships"
6
+
7
+ belongs_to :article, class_name: "Aven::Article", inverse_of: :article_relationships
8
+ belongs_to :related_article, class_name: "Aven::Article"
9
+
10
+ validates :related_article_id, uniqueness: { scope: :article_id }
11
+ validate :not_self_referential
12
+
13
+ scope :ordered, -> { order(position: :asc) }
14
+
15
+ # Delegate workspace access
16
+ delegate :workspace, :workspace_id, to: :article
17
+
18
+ private
19
+
20
+ def not_self_referential
21
+ if article_id == related_article_id
22
+ errors.add(:related_article, "cannot be the same as the article")
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ # == Schema Information
4
+ #
5
+ # Table name: aven_chat_messages
6
+ #
7
+ # id :bigint not null, primary key
8
+ # completed_at :datetime
9
+ # content :text
10
+ # cost_usd :decimal(10, 6) default(0.0)
11
+ # input_tokens :integer default(0)
12
+ # model :string
13
+ # output_tokens :integer default(0)
14
+ # role :string not null
15
+ # started_at :datetime
16
+ # status :string default("pending")
17
+ # tool_call :jsonb
18
+ # total_tokens :integer default(0)
19
+ # created_at :datetime not null
20
+ # updated_at :datetime not null
21
+ # parent_id :bigint
22
+ # thread_id :bigint not null
23
+ #
24
+ # Indexes
25
+ #
26
+ # index_aven_chat_messages_on_parent_id (parent_id)
27
+ # index_aven_chat_messages_on_role (role)
28
+ # index_aven_chat_messages_on_status (status)
29
+ # index_aven_chat_messages_on_thread_id (thread_id)
30
+ # index_aven_chat_messages_on_thread_id_and_created_at (thread_id,created_at)
31
+ #
32
+ # Foreign Keys
33
+ #
34
+ # fk_rails_... (parent_id => aven_chat_messages.id)
35
+ # fk_rails_... (thread_id => aven_chat_threads.id)
36
+ #
37
+ module Aven
38
+ module Chat
39
+ class Message < Aven::ApplicationRecord
40
+ self.table_name = "aven_chat_messages"
41
+
42
+ belongs_to :thread, class_name: "Aven::Chat::Thread"
43
+ belongs_to :parent, class_name: "Aven::Chat::Message", optional: true
44
+
45
+ has_many :replies,
46
+ class_name: "Aven::Chat::Message",
47
+ foreign_key: :parent_id,
48
+ dependent: :nullify
49
+
50
+ enum :role, {
51
+ user: "user",
52
+ assistant: "assistant",
53
+ tool: "tool",
54
+ system: "system"
55
+ }, prefix: true
56
+
57
+ enum :status, {
58
+ pending: "pending",
59
+ streaming: "streaming",
60
+ success: "success",
61
+ error: "error"
62
+ }, prefix: true, default: :pending
63
+
64
+ validates :thread, :role, presence: true
65
+ validates :content, presence: true, unless: -> { role_assistant? && (status_pending? || status_streaming?) }
66
+
67
+ scope :by_tool_call_id, ->(id) { where("tool_call->>'id' = ?", id) }
68
+ scope :chronological, -> { order(:created_at) }
69
+
70
+ after_create_commit :broadcast_created
71
+ after_update_commit :broadcast_updated
72
+
73
+ # Calculate duration if timing is available
74
+ def duration
75
+ return nil unless started_at && completed_at
76
+
77
+ completed_at - started_at
78
+ end
79
+
80
+ def mark_started!
81
+ update!(started_at: Time.current, status: :streaming)
82
+ end
83
+
84
+ def mark_completed!(content: nil, model: nil, tokens: {})
85
+ update!(
86
+ completed_at: Time.current,
87
+ status: :success,
88
+ content:,
89
+ model:,
90
+ input_tokens: tokens[:input] || 0,
91
+ output_tokens: tokens[:output] || 0,
92
+ total_tokens: tokens[:total] || 0
93
+ )
94
+ end
95
+
96
+ def mark_failed!(error_message)
97
+ update!(
98
+ completed_at: Time.current,
99
+ status: :error,
100
+ content: error_message
101
+ )
102
+ end
103
+
104
+ # Append content during streaming
105
+ def append_content!(chunk)
106
+ new_content = (content || "") + chunk
107
+ update_column(:content, new_content)
108
+ broadcast_streaming(new_content)
109
+ end
110
+
111
+ private
112
+
113
+ def broadcast_created
114
+ Aven::Chat::ThreadChannel.broadcast_to(thread, {
115
+ type: "message_created",
116
+ message: as_json
117
+ })
118
+ end
119
+
120
+ def broadcast_updated
121
+ Aven::Chat::ThreadChannel.broadcast_to(thread, {
122
+ type: "message_updated",
123
+ message: as_json
124
+ })
125
+ end
126
+
127
+ def broadcast_streaming(content)
128
+ Aven::Chat::ThreadChannel.broadcast_to(thread, {
129
+ type: "message_streaming",
130
+ message: { id:, content: }
131
+ })
132
+ end
133
+ end
134
+ end
135
+ end