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,110 @@
1
+ <%= ui("page", title: article.title) do |page| %>
2
+ <% page.with_actions_area do %>
3
+ <%= ui("button", label: "Edit", href: routes.edit_article_path(article), variant: :outline) %>
4
+ <%= button_to "Delete",
5
+ routes.article_path(article),
6
+ method: :delete,
7
+ form: { data: { turbo_confirm: "Are you sure you want to delete this article?" } },
8
+ class: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 border border-red-300 text-red-700 bg-white hover:bg-red-50" %>
9
+ <%= ui("button", label: "Back to Articles", href: routes.articles_path, variant: :outline) %>
10
+ <% end %>
11
+
12
+ <div class="max-w-4xl space-y-8">
13
+ <%# Status and metadata %>
14
+ <div class="flex items-center gap-4 text-sm text-gray-500">
15
+ <% badge = status_badge %>
16
+ <%= ui("badge", label: badge[:label], variant: badge[:variant]) %>
17
+
18
+ <% if article.author %>
19
+ <span>By <%= article.author.name %></span>
20
+ <% end %>
21
+
22
+ <% if article.published_at %>
23
+ <span><%= format_date(article.published_at) %></span>
24
+ <% end %>
25
+ </div>
26
+
27
+ <%# Tags %>
28
+ <% if article.tag_list.any? %>
29
+ <div class="flex flex-wrap gap-2">
30
+ <% article.tag_list.each do |tag| %>
31
+ <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
32
+ <%= tag %>
33
+ </span>
34
+ <% end %>
35
+ </div>
36
+ <% end %>
37
+
38
+ <%# Main visual %>
39
+ <% if article.main_visual.attached? %>
40
+ <div class="rounded-lg overflow-hidden">
41
+ <%= image_tag article.main_visual, class: "w-full h-auto" %>
42
+ </div>
43
+ <% end %>
44
+
45
+ <%# Introduction %>
46
+ <% if article.intro.present? %>
47
+ <div class="text-xl text-gray-600 leading-relaxed border-l-4 border-blue-500 pl-4">
48
+ <%= article.intro %>
49
+ </div>
50
+ <% end %>
51
+
52
+ <%# Content %>
53
+ <% if article.description.present? %>
54
+ <div class="prose prose-lg max-w-none">
55
+ <%= rendered_description %>
56
+ </div>
57
+ <% end %>
58
+
59
+ <%# Attachments %>
60
+ <% if article.article_attachments.any? %>
61
+ <div class="border-t pt-8">
62
+ <h3 class="text-lg font-semibold mb-4">Attachments</h3>
63
+ <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
64
+ <% article.sorted_attachments.each do |attachment| %>
65
+ <% if attachment.file.attached? %>
66
+ <div class="relative group">
67
+ <% if attachment.file.image? %>
68
+ <%= link_to url_for(attachment.file), target: "_blank", class: "block" do %>
69
+ <%= image_tag attachment.file.variant(resize_to_limit: [200, 200]),
70
+ class: "w-full h-32 object-cover rounded-lg" %>
71
+ <% end %>
72
+ <% else %>
73
+ <%= link_to url_for(attachment.file), target: "_blank",
74
+ class: "flex items-center justify-center w-full h-32 bg-gray-100 rounded-lg hover:bg-gray-200" do %>
75
+ <div class="text-center">
76
+ <svg class="mx-auto h-8 w-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
77
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
78
+ </svg>
79
+ <span class="text-xs text-gray-500 mt-1 block truncate px-2">
80
+ <%= attachment.file.filename %>
81
+ </span>
82
+ </div>
83
+ <% end %>
84
+ <% end %>
85
+ </div>
86
+ <% end %>
87
+ <% end %>
88
+ </div>
89
+ </div>
90
+ <% end %>
91
+
92
+ <%# Related articles %>
93
+ <% if article.related_articles.any? %>
94
+ <div class="border-t pt-8">
95
+ <h3 class="text-lg font-semibold mb-4">Related Articles</h3>
96
+ <div class="grid gap-4">
97
+ <% article.related_articles.each do |related| %>
98
+ <%= link_to routes.article_path(related),
99
+ class: "block p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition" do %>
100
+ <h4 class="font-medium text-gray-900"><%= related.title %></h4>
101
+ <% if related.intro.present? %>
102
+ <p class="text-sm text-gray-500 mt-1 line-clamp-2"><%= related.intro %></p>
103
+ <% end %>
104
+ <% end %>
105
+ <% end %>
106
+ </div>
107
+ </div>
108
+ <% end %>
109
+ </div>
110
+ <% end %>
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aven::Views::Articles::Show
4
+ class Component < Aven::ApplicationViewComponent
5
+ option :article
6
+ option :current_user, optional: true
7
+
8
+ private
9
+
10
+ def routes
11
+ Aven::Engine.routes.url_helpers
12
+ end
13
+
14
+ def status_badge
15
+ if article.published?
16
+ { label: "Published", variant: :success }
17
+ elsif article.scheduled?
18
+ { label: "Scheduled", variant: :warning }
19
+ else
20
+ { label: "Draft", variant: :secondary }
21
+ end
22
+ end
23
+
24
+ def format_date(date)
25
+ return nil unless date
26
+ date.strftime("%B %d, %Y at %l:%M %p")
27
+ end
28
+
29
+ def rendered_description
30
+ # Simple markdown rendering - could use a proper markdown renderer
31
+ simple_format(article.description)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,44 @@
1
+ <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
2
+ <div class="max-w-md w-full space-y-8">
3
+ <div>
4
+ <h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
5
+ Authentication Failed
6
+ </h2>
7
+ <div class="mt-4 bg-red-50 border border-red-200 rounded-md p-4">
8
+ <div class="flex">
9
+ <div class="flex-shrink-0">
10
+ <svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
11
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
12
+ </svg>
13
+ </div>
14
+ <div class="ml-3">
15
+ <h3 class="text-sm font-medium text-red-800">
16
+ <%= error_message %>
17
+ </h3>
18
+ <% if error_class.present? %>
19
+ <p class="mt-2 text-xs text-red-700">
20
+ Error type: <%= error_class %>
21
+ </p>
22
+ <% end %>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </div>
27
+
28
+ <div class="space-y-4">
29
+ <% provider_links.each do |provider| %>
30
+ <%= link_to(
31
+ "Try again with #{provider.to_s.capitalize}",
32
+ oauth_path_for(provider),
33
+ class: "group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
34
+ ) %>
35
+ <% end %>
36
+
37
+ <%= link_to(
38
+ "Back to home",
39
+ home_path,
40
+ class: "group relative w-full flex justify-center py-2 px-4 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
41
+ ) %>
42
+ </div>
43
+ </div>
44
+ </div>
@@ -0,0 +1,30 @@
1
+ module Aven::Views::Oauth::Error
2
+ class Component < Aven::ApplicationViewComponent
3
+ option :error_message
4
+ option :error_class, optional: true
5
+ option :current_user, optional: true
6
+
7
+ def provider_links
8
+ Aven.configuration.oauth_providers.keys
9
+ end
10
+
11
+ def oauth_path_for(provider)
12
+ case provider.to_sym
13
+ when :github
14
+ Aven::Engine.routes.url_helpers.oauth_github_path
15
+ when :google
16
+ Aven::Engine.routes.url_helpers.oauth_google_path
17
+ else
18
+ "#"
19
+ end
20
+ end
21
+
22
+ def home_path
23
+ if helpers.respond_to?(:main_app) && helpers.main_app.respond_to?(:root_path)
24
+ helpers.main_app.root_path
25
+ else
26
+ Aven::Engine.routes.url_helpers.root_path
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,17 @@
1
+ <%= ui("button", label: "ok") %>
2
+
3
+ <div data-controller="<%= controller_name %>"></div>
4
+
5
+ <% if current_user %>
6
+ <div><%= current_user.id %></div>
7
+ <%= link_to("Logout", Aven::Engine.routes.url_helpers.logout_path) %>
8
+ <% else %>
9
+ <% Aven.configuration.oauth_providers.each do |provider, config| %>
10
+ <%=
11
+ link_to(
12
+ "Login with #{provider.to_s.capitalize}",
13
+ oauth_path_for(provider)
14
+ )
15
+ %>
16
+ <% end %>
17
+ <% end %>
@@ -0,0 +1,16 @@
1
+ module Aven::Views::Static::Index
2
+ class Component < Aven::ApplicationViewComponent
3
+ option(:current_user, optional: true)
4
+
5
+ def oauth_path_for(provider)
6
+ case provider.to_sym
7
+ when :github
8
+ Aven::Engine.routes.url_helpers.oauth_github_path
9
+ when :google
10
+ Aven::Engine.routes.url_helpers.oauth_google_path
11
+ else
12
+ "#"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ connect() {
5
+ console.log("static/index connected");
6
+ }
7
+ }
@@ -0,0 +1,16 @@
1
+ module Aven
2
+ module Admin
3
+ class Base < ApplicationController
4
+ layout("aven/admin")
5
+ before_action(:authenticate_admin!)
6
+
7
+ private
8
+
9
+ def authenticate_admin!
10
+ unless current_user&.admin
11
+ redirect_to(root_path)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,9 @@
1
+ module Aven
2
+ module Admin
3
+ class DashboardController < Base
4
+ def index
5
+ view_component("admin/dashboard/index", current_user:)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aven
4
+ module Agentic
5
+ class AgentsController < Aven::ApplicationController
6
+ before_action :authenticate_user!
7
+ before_action :set_agent, only: [:show, :update, :destroy]
8
+
9
+ def index
10
+ @agents = current_workspace.aven_agentic_agents.enabled.order(:label)
11
+ render json: @agents
12
+ end
13
+
14
+ def show
15
+ render json: @agent.as_json(include: [:tools, :documents])
16
+ end
17
+
18
+ def create
19
+ @agent = current_workspace.aven_agentic_agents.build(agent_params)
20
+
21
+ if @agent.save
22
+ render json: @agent, status: :created
23
+ else
24
+ render json: { errors: @agent.errors }, status: :unprocessable_entity
25
+ end
26
+ end
27
+
28
+ def update
29
+ if @agent.update(agent_params)
30
+ render json: @agent
31
+ else
32
+ render json: { errors: @agent.errors }, status: :unprocessable_entity
33
+ end
34
+ end
35
+
36
+ def destroy
37
+ @agent.destroy
38
+ head :no_content
39
+ end
40
+
41
+ private
42
+
43
+ def set_agent
44
+ @agent = current_workspace.aven_agentic_agents.find(params[:id])
45
+ end
46
+
47
+ def agent_params
48
+ params.require(:agent).permit(
49
+ :label, :system_prompt, :user_facing_question, :enabled,
50
+ agent_tools_attributes: [:id, :tool_id, :_destroy],
51
+ agent_documents_attributes: [:id, :document_id, :_destroy]
52
+ )
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aven
4
+ module Agentic
5
+ class DocumentsController < Aven::ApplicationController
6
+ before_action :authenticate_user!
7
+ before_action :set_document, only: [:show, :destroy]
8
+
9
+ def index
10
+ @documents = current_workspace.aven_agentic_documents.recent
11
+ render json: @documents
12
+ end
13
+
14
+ def show
15
+ render json: @document
16
+ end
17
+
18
+ def create
19
+ @document = current_workspace.aven_agentic_documents.build(document_params)
20
+
21
+ if params[:file].present?
22
+ @document.file.attach(params[:file])
23
+ @document.filename = params[:file].original_filename
24
+ @document.content_type = params[:file].content_type
25
+ @document.byte_size = params[:file].size
26
+ end
27
+
28
+ if @document.save
29
+ render json: @document, status: :created
30
+ else
31
+ render json: { errors: @document.errors }, status: :unprocessable_entity
32
+ end
33
+ end
34
+
35
+ def destroy
36
+ @document.destroy
37
+ head :no_content
38
+ end
39
+
40
+ private
41
+
42
+ def set_document
43
+ @document = current_workspace.aven_agentic_documents.find(params[:id])
44
+ end
45
+
46
+ def document_params
47
+ params.permit(:filename)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aven
4
+ module Agentic
5
+ class McpController < Aven::ApplicationController
6
+ include ActionController::Live
7
+
8
+ skip_before_action :verify_authenticity_token
9
+ before_action :authenticate_mcp_request
10
+
11
+ # Single endpoint handling all MCP methods
12
+ def handle
13
+ case request.method
14
+ when "POST" then handle_post
15
+ when "GET" then handle_sse
16
+ when "DELETE" then handle_delete
17
+ end
18
+ end
19
+
20
+ # Health check endpoint
21
+ def health
22
+ render json: {
23
+ status: "ok",
24
+ server: Mcp::ServerFactory::SERVER_NAME,
25
+ version: Mcp::ServerFactory::SERVER_VERSION,
26
+ timestamp: Time.current.iso8601
27
+ }
28
+ end
29
+
30
+ private
31
+
32
+ def handle_post
33
+ server = build_server
34
+ return render_mcp_error("Server not available", -32603) unless server
35
+
36
+ request_body = request.body.read
37
+ Rails.logger.debug { "[Aven::MCP] Request: #{request_body}" }
38
+
39
+ result_json = server.handle_json(request_body)
40
+
41
+ if result_json.nil?
42
+ return head :no_content
43
+ end
44
+
45
+ Rails.logger.debug { "[Aven::MCP] Response: #{result_json}" }
46
+
47
+ result_hash = JSON.parse(result_json)
48
+ if initialization_response?(result_hash)
49
+ response.headers["Mcp-Session-Id"] = SecureRandom.uuid
50
+ end
51
+
52
+ render json: result_json, status: :ok
53
+ rescue JSON::ParserError => e
54
+ render_mcp_error("Parse error: #{e.message}", -32700, status: :bad_request)
55
+ rescue => e
56
+ Rails.logger.error { "[Aven::MCP] Error: #{e.message}" }
57
+ render_mcp_error("Internal error", -32603, status: :internal_server_error)
58
+ end
59
+
60
+ def handle_sse
61
+ response.headers["Content-Type"] = "text/event-stream"
62
+ response.headers["Cache-Control"] = "no-cache"
63
+ response.headers["Connection"] = "keep-alive"
64
+
65
+ sse = ActionController::Live::SSE.new(response.stream, event: "message")
66
+ session_id = request.headers["Mcp-Session-Id"]
67
+
68
+ loop do
69
+ sse.write({ type: "ping", timestamp: Time.current.iso8601 })
70
+ sleep 30
71
+ end
72
+ rescue ActionController::Live::ClientDisconnected
73
+ Rails.logger.info { "[Aven::MCP] SSE client disconnected" }
74
+ ensure
75
+ sse&.close
76
+ end
77
+
78
+ def handle_delete
79
+ head :ok
80
+ end
81
+
82
+ def build_server
83
+ Mcp::ServerFactory.build(server_context: {
84
+ workspace: @workspace,
85
+ api_token: @api_token
86
+ })
87
+ end
88
+
89
+ def authenticate_mcp_request
90
+ token = extract_token
91
+ return render_mcp_error("Unauthorized", -32001, status: :unauthorized) if token.blank?
92
+
93
+ @api_token = validate_token(token)
94
+ render_mcp_error("Unauthorized", -32001, status: :unauthorized) unless @api_token
95
+ end
96
+
97
+ def extract_token
98
+ auth_header = request.headers["Authorization"]
99
+ return auth_header.delete_prefix("Bearer ").strip if auth_header&.start_with?("Bearer ")
100
+
101
+ request.headers["X-Api-Key"]&.strip || params[:token]&.strip
102
+ end
103
+
104
+ def validate_token(token)
105
+ env_token = ENV.fetch("AVEN_MCP_API_TOKEN", nil)
106
+ if env_token.present? && ActiveSupport::SecurityUtils.secure_compare(token, env_token)
107
+ OpenStruct.new(valid: true, source: :env)
108
+ end
109
+ end
110
+
111
+ def render_mcp_error(message, code, status: :ok)
112
+ render json: {
113
+ jsonrpc: "2.0",
114
+ error: { code:, message: },
115
+ id: nil
116
+ }, status:
117
+ end
118
+
119
+ def initialization_response?(result)
120
+ result.dig("result", "protocolVersion").present?
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aven
4
+ module Agentic
5
+ class ToolsController < Aven::ApplicationController
6
+ before_action :authenticate_user!
7
+ before_action :set_tool, only: [:show, :update]
8
+
9
+ def index
10
+ @tools = Aven::Agentic::Tool.for_workspace(current_workspace).enabled.order(:name)
11
+ render json: @tools.as_json(include: :parameters)
12
+ end
13
+
14
+ def show
15
+ render json: @tool.as_json(include: :parameters)
16
+ end
17
+
18
+ def update
19
+ if @tool.update(tool_params)
20
+ render json: @tool
21
+ else
22
+ render json: { errors: @tool.errors }, status: :unprocessable_entity
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def set_tool
29
+ @tool = Aven::Agentic::Tool.for_workspace(current_workspace).find(params[:id])
30
+ end
31
+
32
+ def tool_params
33
+ params.require(:tool).permit(:description, :enabled)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aven
4
+ module Ai
5
+ class TextController < Aven::ApplicationController
6
+ include Aven::Authentication
7
+ include ActionController::Live
8
+
9
+ before_action :authenticate_user!
10
+
11
+ def generate
12
+ response.headers["Content-Type"] = "text/event-stream"
13
+ response.headers["Cache-Control"] = "no-cache"
14
+ response.headers["X-Accel-Buffering"] = "no"
15
+
16
+ model = params[:model] || "gpt-4o-mini"
17
+ chat = RubyLLM.chat(model:)
18
+
19
+ # Apply system prompts if provided
20
+ if params[:system_prompts].present?
21
+ Array(params[:system_prompts]).each do |system_prompt|
22
+ chat.with_instructions(system_prompt)
23
+ end
24
+ end
25
+
26
+ # Stream the response
27
+ chat.ask(params[:prompt]) do |chunk|
28
+ content = chunk.respond_to?(:content) ? chunk.content : chunk.to_s
29
+ response.stream.write "data: #{content.to_json}\n\n"
30
+ end
31
+
32
+ response.stream.write "data: [DONE]\n\n"
33
+ rescue => e
34
+ Rails.logger.error "AI generation error: #{e.message}"
35
+ response.stream.write "data: #{JSON.generate(error: e.message)}\n\n"
36
+ ensure
37
+ response.stream.close
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,27 @@
1
+ module Aven
2
+ class ApplicationController < ActionController::Base
3
+ include Aven::ApplicationHelper
4
+
5
+ helper_method :current_workspace
6
+
7
+ # Get the current workspace from session
8
+ def current_workspace
9
+ return @current_workspace if defined?(@current_workspace)
10
+
11
+ @current_workspace = if session[:workspace_id].present? && current_user
12
+ current_user.workspaces.find_by(id: session[:workspace_id])
13
+ elsif current_user
14
+ # Auto-select first workspace if none selected
15
+ workspace = current_user.workspaces.first
16
+ session[:workspace_id] = workspace&.id
17
+ workspace
18
+ end
19
+ end
20
+
21
+ # Set the current workspace
22
+ def current_workspace=(workspace)
23
+ @current_workspace = workspace
24
+ session[:workspace_id] = workspace&.id
25
+ end
26
+ end
27
+ end