collavre 0.15.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/collavre/code_highlight.css +13 -0
- data/app/assets/stylesheets/collavre/comments_popup.css +65 -5
- data/app/assets/stylesheets/collavre/creatives.css +5 -2
- data/app/controllers/collavre/admin/settings_controller.rb +9 -0
- data/app/controllers/collavre/attachments_controller.rb +1 -1
- data/app/controllers/collavre/comment_read_pointers_controller.rb +2 -2
- data/app/controllers/collavre/comments/snapshots_controller.rb +2 -7
- data/app/controllers/collavre/comments_controller.rb +23 -25
- data/app/controllers/collavre/creatives_controller.rb +22 -11
- data/app/controllers/collavre/emails_controller.rb +2 -0
- data/app/controllers/collavre/google_auth_controller.rb +1 -1
- data/app/controllers/collavre/inbox_items_controller.rb +1 -1
- data/app/controllers/collavre/invites_controller.rb +3 -0
- data/app/controllers/collavre/tasks_controller.rb +1 -1
- data/app/controllers/collavre/topics_controller.rb +27 -85
- data/app/controllers/concerns/collavre/comments/comment_scoping.rb +3 -0
- data/app/controllers/concerns/collavre/creative_permission_guard.rb +32 -0
- data/app/controllers/concerns/collavre/integration_setup.rb +17 -0
- data/app/helpers/collavre/application_helper.rb +30 -2
- data/app/javascript/components/InlineLexicalEditor.jsx +7 -3
- data/app/javascript/components/creative_tree_row.js +21 -1
- data/app/javascript/components/plugins/markdown_shortcuts_plugin.jsx +34 -0
- data/app/javascript/controllers/comment_controller.js +19 -1
- data/app/javascript/controllers/comment_version_controller.js +2 -1
- data/app/javascript/controllers/comments/form_controller.js +7 -4
- data/app/javascript/controllers/comments/list_controller.js +43 -4
- data/app/javascript/controllers/comments/popup_controller.js +45 -12
- data/app/javascript/controllers/comments/presence_controller.js +8 -0
- data/app/javascript/controllers/comments/topics_controller.js +50 -31
- data/app/javascript/creatives/tree_renderer.js +1 -0
- data/app/javascript/lib/__tests__/chat_history.test.js +31 -0
- data/app/javascript/lib/chat_history.js +12 -2
- data/app/javascript/lib/utils/markdown.js +32 -1
- data/app/javascript/modules/command_args_form.js +8 -0
- data/app/javascript/modules/creative_row_editor.js +12 -17
- data/app/javascript/modules/integration_wizard.js +162 -0
- data/app/jobs/collavre/compress_job.rb +1 -0
- data/app/jobs/collavre/creative_broadcast_job.rb +4 -1
- data/app/jobs/collavre/merge_comments_job.rb +1 -0
- data/app/jobs/collavre/trigger_loop_check_job.rb +1 -0
- data/app/jobs/collavre/trigger_loop_verify_job.rb +1 -0
- data/app/models/collavre/calendar_event.rb +0 -4
- data/app/models/collavre/comment/broadcastable.rb +1 -1
- data/app/models/collavre/comment.rb +17 -2
- data/app/models/collavre/comment_snapshot.rb +0 -1
- data/app/models/collavre/creative/describable.rb +10 -1
- data/app/models/collavre/creative/realtime_broadcastable.rb +17 -5
- data/app/models/collavre/creative.rb +43 -1
- data/app/models/collavre/current.rb +1 -1
- data/app/models/collavre/inbox_item.rb +0 -4
- data/app/models/collavre/system_setting.rb +10 -1
- data/app/models/collavre/task.rb +17 -8
- data/app/models/collavre/user.rb +11 -1
- data/app/models/concerns/collavre/ai_agent_resolvable.rb +0 -8
- data/app/services/collavre/ai_agent/message_builder.rb +32 -15
- data/app/services/collavre/ai_agent/response_finalizer.rb +2 -1
- data/app/services/collavre/ai_agent/session_context_resolver.rb +50 -0
- data/app/services/collavre/ai_agent_service.rb +14 -2
- data/app/services/collavre/ai_client.rb +13 -0
- data/app/services/collavre/command_menu_service.rb +23 -3
- data/app/services/collavre/comments/command_processor.rb +1 -1
- data/app/services/collavre/comments/mcp_command.rb +27 -8
- data/app/services/collavre/comments/mcp_command_builder.rb +4 -3
- data/app/services/collavre/creatives/tree_builder.rb +3 -0
- data/app/services/collavre/google_calendar_service.rb +32 -6
- data/app/services/collavre/markdown_converter.rb +15 -20
- data/app/services/collavre/mcp_service.rb +4 -4
- data/app/services/collavre/orchestration/agent_orchestrator.rb +2 -2
- data/app/services/collavre/tools/creative_batch_service.rb +5 -1
- data/app/services/collavre/tools/cron_create_service.rb +9 -6
- data/app/services/collavre/topic_branch_service.rb +2 -2
- data/app/views/collavre/admin/settings/_system_tab.html.erb +11 -0
- data/app/views/collavre/comments/_comment.html.erb +7 -1
- data/app/views/collavre/comments/_comments_popup.html.erb +4 -1
- data/config/locales/admin.en.yml +3 -0
- data/config/locales/admin.ko.yml +3 -0
- data/config/locales/comments.en.yml +6 -1
- data/config/locales/comments.ko.yml +6 -1
- data/db/migrate/20251126040752_add_description_to_creatives.rb +1 -1
- data/db/migrate/20260415000000_create_main_topics_for_existing_creatives.rb +69 -0
- data/lib/collavre/version.rb +1 -1
- metadata +21 -4
- data/app/jobs/collavre/permission_cache_cleanup_job.rb +0 -36
- data/app/models/collavre/variation.rb +0 -5
- data/app/services/collavre/creatives/path_exporter.rb +0 -131
|
@@ -43,14 +43,15 @@ module Collavre
|
|
|
43
43
|
topic = current_topic
|
|
44
44
|
topic_info = topic ? "\nTopic: #{topic.name} (id: #{topic.id})" : ""
|
|
45
45
|
|
|
46
|
+
ancestry = build_ancestry_chain(creative)
|
|
47
|
+
|
|
46
48
|
if effective.data&.dig("disabled_self_context") == true
|
|
47
49
|
# Self-context disabled: inject only the ancestry chain so the AI
|
|
48
50
|
# knows where in the hierarchy the conversation is happening
|
|
49
|
-
ancestry = build_ancestry_chain(creative)
|
|
50
51
|
@injected_creative_ids << creative.id
|
|
51
|
-
messages << { role: "user", kind: :creative_context, parts: [ { text: "
|
|
52
|
+
messages << { role: "user", kind: :creative_context, parts: [ { text: "Creative Path: #{ancestry}#{topic_info}" } ] }
|
|
52
53
|
else
|
|
53
|
-
# Full self-context: inject the creative subtree
|
|
54
|
+
# Full self-context: inject the creative subtree with ancestry breadcrumb
|
|
54
55
|
children_level = @agent.creative_children_level
|
|
55
56
|
max_depth = 1 + children_level
|
|
56
57
|
markdown = ApplicationController.helpers.render_creative_tree_markdown(
|
|
@@ -58,12 +59,12 @@ module Collavre
|
|
|
58
59
|
)
|
|
59
60
|
|
|
60
61
|
@injected_creative_ids << creative.id
|
|
61
|
-
messages << { role: "user", kind: :creative_context, parts: [ { text: "Creative
|
|
62
|
+
messages << { role: "user", kind: :creative_context, parts: [ { text: "Creative Path: #{ancestry}#{topic_info}\n#{markdown}" } ] }
|
|
62
63
|
end
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
def build_ancestry_chain(creative)
|
|
66
|
-
creative.self_and_ancestors.reverse.map(
|
|
67
|
+
creative.self_and_ancestors.reverse.map { |c| "#{c.creative_snippet} (id: #{c.id})" }.join(" > ")
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
def append_context_creatives(messages)
|
|
@@ -75,17 +76,20 @@ module Collavre
|
|
|
75
76
|
|
|
76
77
|
effective_origin = creative.effective_origin(Set.new)
|
|
77
78
|
context_ids = effective_origin.effective_context_ids
|
|
78
|
-
disabled_ids =
|
|
79
|
+
disabled_ids = effective_origin.effective_disabled_context_ids
|
|
79
80
|
active_ids = context_ids - disabled_ids - [ creative_id, effective_origin.id ]
|
|
80
81
|
return if active_ids.empty?
|
|
81
82
|
|
|
82
83
|
children_level = @agent.creative_children_level
|
|
83
84
|
max_depth = 1 + children_level
|
|
84
85
|
|
|
86
|
+
ids_to_load = active_ids.reject { |ctx_id| @injected_creative_ids.include?(ctx_id) }
|
|
87
|
+
creatives_by_id = Creative.where(id: ids_to_load).index_by(&:id)
|
|
88
|
+
|
|
85
89
|
active_ids.each do |ctx_id|
|
|
86
90
|
next if @injected_creative_ids.include?(ctx_id)
|
|
87
91
|
|
|
88
|
-
ctx =
|
|
92
|
+
ctx = creatives_by_id[ctx_id]
|
|
89
93
|
next unless ctx
|
|
90
94
|
|
|
91
95
|
@injected_creative_ids << ctx_id
|
|
@@ -109,11 +113,12 @@ module Collavre
|
|
|
109
113
|
max_depth = 1 + children_level
|
|
110
114
|
|
|
111
115
|
# Extract creative IDs from markdown links like [title](/creatives/123)
|
|
112
|
-
content.scan(%r{\[[^\]]*\]\(/creatives/(\d+)\)}).flatten.uniq
|
|
113
|
-
|
|
114
|
-
|
|
116
|
+
referenced_ids = content.scan(%r{\[[^\]]*\]\(/creatives/(\d+)\)}).flatten.map(&:to_i).uniq
|
|
117
|
+
referenced_ids.reject! { |cid| @injected_creative_ids.include?(cid) }
|
|
118
|
+
creatives_by_id = Creative.where(id: referenced_ids).index_by(&:id)
|
|
115
119
|
|
|
116
|
-
|
|
120
|
+
referenced_ids.each do |creative_id|
|
|
121
|
+
creative = creatives_by_id[creative_id]
|
|
117
122
|
next unless creative
|
|
118
123
|
|
|
119
124
|
@injected_creative_ids << creative_id
|
|
@@ -141,7 +146,7 @@ module Collavre
|
|
|
141
146
|
history_chars = 0
|
|
142
147
|
count = 0
|
|
143
148
|
|
|
144
|
-
Comment.where(creative_id: creative_id
|
|
149
|
+
Comment.public_only.where(creative_id: creative_id)
|
|
145
150
|
.where(topic_id: topic_id)
|
|
146
151
|
.where.not(user_id: nil)
|
|
147
152
|
.includes(:user)
|
|
@@ -247,9 +252,21 @@ module Collavre
|
|
|
247
252
|
# This is slightly broader than what's actually rendered (which is
|
|
248
253
|
# limited by children_level), but ensures no descendant change is missed.
|
|
249
254
|
def collect_rendered_creative_ids
|
|
250
|
-
@injected_creative_ids.
|
|
251
|
-
|
|
252
|
-
|
|
255
|
+
roots = Creative.where(id: @injected_creative_ids.to_a).to_a
|
|
256
|
+
found_ids = roots.map(&:id).to_set
|
|
257
|
+
|
|
258
|
+
ids = roots.flat_map(&:subtree_ids)
|
|
259
|
+
.concat(@injected_creative_ids.reject { |rid| found_ids.include?(rid) })
|
|
260
|
+
|
|
261
|
+
# Include ancestor IDs used in the breadcrumb so that changes to
|
|
262
|
+
# ancestor titles are detected by context_changed?
|
|
263
|
+
creative_id = @context.dig("creative", "id")
|
|
264
|
+
if creative_id
|
|
265
|
+
ancestor_ids = Creative.find_by(id: creative_id)&.ancestor_ids || []
|
|
266
|
+
ids.concat(ancestor_ids)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
ids.uniq
|
|
253
270
|
end
|
|
254
271
|
end
|
|
255
272
|
end
|
|
@@ -72,7 +72,8 @@ module Collavre
|
|
|
72
72
|
reply_comment = @original_comment.creative.comments.create!(
|
|
73
73
|
content: @response_content,
|
|
74
74
|
user: @agent,
|
|
75
|
-
topic_id: @original_comment.topic_id
|
|
75
|
+
topic_id: @original_comment.topic_id,
|
|
76
|
+
task: @task
|
|
76
77
|
)
|
|
77
78
|
|
|
78
79
|
log_action("reply_created", { comment_id: reply_comment.id, content: @response_content })
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
module AiAgent
|
|
5
|
+
class SessionContextResolver
|
|
6
|
+
def initialize(agent:, messages_data:, system_prompt:)
|
|
7
|
+
@agent = agent
|
|
8
|
+
@messages_data = messages_data
|
|
9
|
+
@system_prompt = system_prompt
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def resolve
|
|
13
|
+
if @agent.supports_session? && !needs_full_context?
|
|
14
|
+
incremental_payload
|
|
15
|
+
else
|
|
16
|
+
full_payload
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def needs_full_context?
|
|
23
|
+
@messages_data[:first_message] || @messages_data[:context_changed]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def full_payload
|
|
27
|
+
msgs = @messages_data[:messages]
|
|
28
|
+
msgs = msgs.reject { |m| m[:kind] == :chat_history } if @agent.supports_session?
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
messages: msgs,
|
|
32
|
+
system_prompt: @system_prompt,
|
|
33
|
+
first_message: @messages_data[:first_message],
|
|
34
|
+
context_changed: @messages_data[:context_changed]
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def incremental_payload
|
|
39
|
+
trigger_only = @messages_data[:messages].select { |m| m[:kind] == :trigger }
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
messages: trigger_only,
|
|
43
|
+
system_prompt: nil,
|
|
44
|
+
first_message: false,
|
|
45
|
+
context_changed: false
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -29,6 +29,9 @@ module Collavre
|
|
|
29
29
|
rendering_context = prepare_rendering_context
|
|
30
30
|
system_prompt = render_system_prompt(rendering_context)
|
|
31
31
|
|
|
32
|
+
# Resolve session context (decides what to send)
|
|
33
|
+
resolved = resolve_session_context(messages_data, system_prompt)
|
|
34
|
+
|
|
32
35
|
# Create placeholder comment if needed
|
|
33
36
|
@reply_comment = create_reply_comment_if_needed
|
|
34
37
|
|
|
@@ -48,8 +51,8 @@ module Collavre
|
|
|
48
51
|
@lifecycle_manager.broadcast_status("thinking")
|
|
49
52
|
|
|
50
53
|
# Execute AI chat with streaming
|
|
51
|
-
@client = build_ai_client(system_prompt)
|
|
52
|
-
stream_response(@client,
|
|
54
|
+
@client = build_ai_client(resolved[:system_prompt])
|
|
55
|
+
stream_response(@client, resolved)
|
|
53
56
|
|
|
54
57
|
log_action("completion", { response: @streamer.content })
|
|
55
58
|
|
|
@@ -89,6 +92,14 @@ module Collavre
|
|
|
89
92
|
).build
|
|
90
93
|
end
|
|
91
94
|
|
|
95
|
+
def resolve_session_context(messages_data, system_prompt)
|
|
96
|
+
AiAgent::SessionContextResolver.new(
|
|
97
|
+
agent: @agent,
|
|
98
|
+
messages_data: messages_data,
|
|
99
|
+
system_prompt: system_prompt
|
|
100
|
+
).resolve
|
|
101
|
+
end
|
|
102
|
+
|
|
92
103
|
def find_creative
|
|
93
104
|
creative_id = @context.dig("creative", "id")
|
|
94
105
|
creative_id ? Creative.find_by(id: creative_id) : nil
|
|
@@ -126,6 +137,7 @@ module Collavre
|
|
|
126
137
|
content: Comment::STREAMING_PLACEHOLDER_CONTENT,
|
|
127
138
|
user: @agent,
|
|
128
139
|
topic_id: @original_comment.topic_id,
|
|
140
|
+
task: @task,
|
|
129
141
|
skip_dispatch: true # A2A routing handled by A2aDispatcher after finalization
|
|
130
142
|
)
|
|
131
143
|
end
|
|
@@ -132,9 +132,14 @@ module Collavre
|
|
|
132
132
|
chat_opts[:provider] = provider if provider
|
|
133
133
|
chat_opts[:assume_model_exists] = true if provider
|
|
134
134
|
|
|
135
|
+
# Apply current system timeout setting (picks up changes without restart)
|
|
136
|
+
RubyLLM.config.request_timeout = SystemSetting.llm_request_timeout_seconds
|
|
137
|
+
|
|
135
138
|
RubyLLM.context(&context_block)
|
|
136
139
|
.chat(**chat_opts).tap do |chat|
|
|
137
140
|
chat.with_instructions(system_prompt) if system_prompt.present?
|
|
141
|
+
session_id = build_session_id
|
|
142
|
+
chat.with_headers("X-Session-Id" => session_id) if session_id
|
|
138
143
|
chat.on_tool_call do |tool_call|
|
|
139
144
|
check_tool_approval!(tool_call)
|
|
140
145
|
end
|
|
@@ -146,6 +151,14 @@ module Collavre
|
|
|
146
151
|
end
|
|
147
152
|
end
|
|
148
153
|
|
|
154
|
+
def build_session_id
|
|
155
|
+
creative_id = context&.dig(:creative)&.id
|
|
156
|
+
topic_id = context&.dig(:comment)&.topic_id || context&.dig(:topic_id)
|
|
157
|
+
return nil unless creative_id && topic_id
|
|
158
|
+
|
|
159
|
+
"creative_#{creative_id}_topic_#{topic_id}"
|
|
160
|
+
end
|
|
161
|
+
|
|
149
162
|
def check_tool_approval!(tool_call)
|
|
150
163
|
tool_name = tool_call.name
|
|
151
164
|
task = context&.dig(:task)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
module Collavre
|
|
2
2
|
class CommandMenuService
|
|
3
|
-
def initialize(user:)
|
|
3
|
+
def initialize(user:, creative: nil)
|
|
4
4
|
@user = user
|
|
5
|
+
@creative = creative
|
|
5
6
|
end
|
|
6
7
|
|
|
7
8
|
def items
|
|
@@ -10,7 +11,7 @@ module Collavre
|
|
|
10
11
|
|
|
11
12
|
private
|
|
12
13
|
|
|
13
|
-
attr_reader :user
|
|
14
|
+
attr_reader :user, :creative
|
|
14
15
|
|
|
15
16
|
def built_in_items
|
|
16
17
|
[
|
|
@@ -70,12 +71,16 @@ module Collavre
|
|
|
70
71
|
next unless tool_name
|
|
71
72
|
|
|
72
73
|
raw_params = tool[:params] || tool["params"]
|
|
74
|
+
schema = normalize_params(raw_params)
|
|
75
|
+
defaults = tool_defaults_for(tool_name)
|
|
76
|
+
apply_defaults_to_schema(schema, defaults) if defaults.present?
|
|
77
|
+
|
|
73
78
|
{
|
|
74
79
|
name: tool_name,
|
|
75
80
|
label: "/#{tool_name}",
|
|
76
81
|
description: tool[:description] || tool["description"],
|
|
77
82
|
args: format_args(raw_params),
|
|
78
|
-
input_schema:
|
|
83
|
+
input_schema: schema
|
|
79
84
|
}
|
|
80
85
|
end
|
|
81
86
|
end
|
|
@@ -111,6 +116,21 @@ module Collavre
|
|
|
111
116
|
end
|
|
112
117
|
end
|
|
113
118
|
|
|
119
|
+
def tool_defaults_for(tool_name)
|
|
120
|
+
creative&.data&.dig("tools", tool_name)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def apply_defaults_to_schema(schema, defaults)
|
|
124
|
+
return unless schema.is_a?(Array) && defaults.is_a?(Hash)
|
|
125
|
+
|
|
126
|
+
schema.each do |param|
|
|
127
|
+
name = param[:name]
|
|
128
|
+
if defaults.key?(name)
|
|
129
|
+
param[:default_value] = defaults[name]
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
114
134
|
def format_args(params)
|
|
115
135
|
return if params.blank?
|
|
116
136
|
|
|
@@ -35,7 +35,7 @@ module Collavre
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def mcp_commands
|
|
38
|
-
Collavre::Comments::McpCommandBuilder.new(comment: comment, user: user).commands
|
|
38
|
+
Collavre::Comments::McpCommandBuilder.new(comment: comment, user: user, creative: comment.creative).commands
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
end
|
|
@@ -3,10 +3,11 @@ module Collavre
|
|
|
3
3
|
|
|
4
4
|
module Comments
|
|
5
5
|
class McpCommand
|
|
6
|
-
def initialize(comment:, user:, tool:, meta_tool_service: default_meta_tool_service)
|
|
6
|
+
def initialize(comment:, user:, tool:, creative: nil, meta_tool_service: default_meta_tool_service)
|
|
7
7
|
@comment = comment
|
|
8
8
|
@user = user
|
|
9
9
|
@tool = tool
|
|
10
|
+
@creative = creative
|
|
10
11
|
@meta_tool_service = meta_tool_service
|
|
11
12
|
end
|
|
12
13
|
|
|
@@ -25,7 +26,7 @@ module Collavre
|
|
|
25
26
|
|
|
26
27
|
private
|
|
27
28
|
|
|
28
|
-
attr_reader :comment, :user, :tool, :meta_tool_service
|
|
29
|
+
attr_reader :comment, :user, :tool, :creative, :meta_tool_service
|
|
29
30
|
|
|
30
31
|
def command_match?
|
|
31
32
|
comment.content.to_s.strip.match?(command_pattern)
|
|
@@ -41,11 +42,17 @@ module Collavre
|
|
|
41
42
|
|
|
42
43
|
def parsed_arguments
|
|
43
44
|
body = command_body
|
|
44
|
-
|
|
45
|
+
explicit_args = if body.blank?
|
|
46
|
+
{}
|
|
47
|
+
else
|
|
48
|
+
begin
|
|
49
|
+
JSON.parse(body)
|
|
50
|
+
rescue JSON::ParserError
|
|
51
|
+
key_value_arguments(body)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
rescue JSON::ParserError
|
|
48
|
-
key_value_arguments(body)
|
|
55
|
+
merge_with_defaults(explicit_args)
|
|
49
56
|
end
|
|
50
57
|
|
|
51
58
|
def key_value_arguments(body)
|
|
@@ -61,6 +68,15 @@ module Collavre
|
|
|
61
68
|
nil
|
|
62
69
|
end
|
|
63
70
|
|
|
71
|
+
def merge_with_defaults(explicit_args)
|
|
72
|
+
return explicit_args unless explicit_args.is_a?(Hash)
|
|
73
|
+
|
|
74
|
+
defaults = creative&.data&.dig("tools", tool_name)
|
|
75
|
+
return explicit_args unless defaults.is_a?(Hash)
|
|
76
|
+
|
|
77
|
+
defaults.merge(explicit_args)
|
|
78
|
+
end
|
|
79
|
+
|
|
64
80
|
def cast_value(value)
|
|
65
81
|
case value
|
|
66
82
|
when /\A\d+\z/ then value.to_i
|
|
@@ -89,9 +105,12 @@ module Collavre
|
|
|
89
105
|
result.to_s
|
|
90
106
|
end
|
|
91
107
|
|
|
108
|
+
escaped_tool = ERB::Util.html_escape(tool_name)
|
|
109
|
+
escaped_content = ERB::Util.html_escape(content)
|
|
110
|
+
|
|
92
111
|
<<~HTML
|
|
93
|
-
<details><summary>#{
|
|
94
|
-
<pre><code>#{
|
|
112
|
+
<details><summary>#{escaped_tool} response</summary>
|
|
113
|
+
<pre><code>#{escaped_content}</code></pre>
|
|
95
114
|
</details>
|
|
96
115
|
HTML
|
|
97
116
|
end
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
module Collavre
|
|
2
2
|
module Comments
|
|
3
3
|
class McpCommandBuilder
|
|
4
|
-
def initialize(comment:, user:, logger: Rails.logger)
|
|
4
|
+
def initialize(comment:, user:, creative: nil, logger: Rails.logger)
|
|
5
5
|
@comment = comment
|
|
6
6
|
@user = user
|
|
7
|
+
@creative = creative
|
|
7
8
|
@logger = logger
|
|
8
9
|
end
|
|
9
10
|
|
|
@@ -13,7 +14,7 @@ module Collavre
|
|
|
13
14
|
RailsMcpEngine::Engine.build_tools!
|
|
14
15
|
tools = meta_tool_service.call(action: "list", tool_name: nil, query: nil, arguments: nil)
|
|
15
16
|
Array(tools[:tools]).map do |tool|
|
|
16
|
-
Collavre::Comments::McpCommand.new(comment: comment, user: user, tool: tool, meta_tool_service: meta_tool_service)
|
|
17
|
+
Collavre::Comments::McpCommand.new(comment: comment, user: user, tool: tool, creative: creative, meta_tool_service: meta_tool_service)
|
|
17
18
|
end
|
|
18
19
|
rescue StandardError => e
|
|
19
20
|
logger.error("MCP command registration failed: #{e.message}")
|
|
@@ -22,7 +23,7 @@ module Collavre
|
|
|
22
23
|
|
|
23
24
|
private
|
|
24
25
|
|
|
25
|
-
attr_reader :comment, :user, :logger
|
|
26
|
+
attr_reader :comment, :user, :creative, :logger
|
|
26
27
|
|
|
27
28
|
def meta_tool_service
|
|
28
29
|
@meta_tool_service ||= ::Tools::MetaToolService.new
|
|
@@ -75,6 +75,8 @@ module Creatives
|
|
|
75
75
|
|
|
76
76
|
def cached_can_write?(creative)
|
|
77
77
|
return false unless user
|
|
78
|
+
# GitHub-synced creatives are always read-only
|
|
79
|
+
return false if creative.github_markdown?
|
|
78
80
|
|
|
79
81
|
if @permission_cache.key?(creative.id)
|
|
80
82
|
@permission_cache[creative.id]
|
|
@@ -122,6 +124,7 @@ module Creatives
|
|
|
122
124
|
expanded: expanded,
|
|
123
125
|
is_root: creative.parent.nil?,
|
|
124
126
|
archived: creative.archived?,
|
|
127
|
+
github_source: creative.github_markdown?,
|
|
125
128
|
sequence: creative.sequence,
|
|
126
129
|
link_url: view_context.collavre.creative_path(creative),
|
|
127
130
|
templates: template_payload_for(creative, has_children: filtered_children.any?),
|
|
@@ -95,6 +95,12 @@ module Collavre
|
|
|
95
95
|
|
|
96
96
|
def list_calendars
|
|
97
97
|
@service.list_calendar_lists.items.map { |c| [ c.summary, c.id ] }.to_h
|
|
98
|
+
rescue Google::Apis::AuthorizationError => e
|
|
99
|
+
Rails.logger.error("Google Calendar list_calendars auth error for user #{@user.id}: #{e.message}")
|
|
100
|
+
raise GoogleCalendarError, I18n.t("collavre.google_calendar.errors.reconnect")
|
|
101
|
+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Google::Apis::TransmissionError => e
|
|
102
|
+
Rails.logger.error("Google Calendar list_calendars network error for user #{@user.id}: #{e.class} - #{e.message}")
|
|
103
|
+
raise GoogleCalendarError, I18n.t("collavre.google_calendar.errors.network")
|
|
98
104
|
end
|
|
99
105
|
|
|
100
106
|
private
|
|
@@ -109,6 +115,12 @@ module Collavre
|
|
|
109
115
|
scope: [ Google::Apis::CalendarV3::AUTH_CALENDAR_APP_CREATED ],
|
|
110
116
|
refresh_token: token
|
|
111
117
|
).tap(&:fetch_access_token!)
|
|
118
|
+
rescue Signet::AuthorizationError => e
|
|
119
|
+
Rails.logger.error("Google Calendar fetch_access_token! auth error for user #{@user.id}: #{e.message}")
|
|
120
|
+
raise GoogleCalendarError, I18n.t("collavre.google_calendar.errors.reconnect")
|
|
121
|
+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
|
|
122
|
+
Rails.logger.error("Google Calendar fetch_access_token! network error for user #{@user.id}: #{e.class} - #{e.message}")
|
|
123
|
+
raise GoogleCalendarError, I18n.t("collavre.google_calendar.errors.network")
|
|
112
124
|
end
|
|
113
125
|
|
|
114
126
|
def refresh_token
|
|
@@ -130,10 +142,11 @@ module Collavre
|
|
|
130
142
|
end
|
|
131
143
|
|
|
132
144
|
def raw_google_refresh_token
|
|
133
|
-
|
|
134
|
-
|
|
145
|
+
ActiveRecord::Base.connection.select_value(
|
|
146
|
+
ActiveRecord::Base.sanitize_sql_array(
|
|
147
|
+
[ "SELECT google_refresh_token FROM users WHERE id = ?", @user.id ]
|
|
148
|
+
)
|
|
135
149
|
)
|
|
136
|
-
result
|
|
137
150
|
end
|
|
138
151
|
|
|
139
152
|
def encrypted_value?(value)
|
|
@@ -160,9 +173,22 @@ module Collavre
|
|
|
160
173
|
if @user.calendar_id.nil?
|
|
161
174
|
@user.calendar_id = create_app_calendar
|
|
162
175
|
else
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
@user.calendar_id
|
|
176
|
+
begin
|
|
177
|
+
calendar = @service.get_calendar(@user.calendar_id)
|
|
178
|
+
if calendar.id != @user.calendar_id
|
|
179
|
+
@user.calendar_id = create_app_calendar
|
|
180
|
+
end
|
|
181
|
+
rescue Google::Apis::ClientError => e
|
|
182
|
+
if e.status_code == 404
|
|
183
|
+
Rails.logger.error("Google Calendar not found (deleted?) for user #{@user.id}, calendar_id=#{@user.calendar_id}")
|
|
184
|
+
@user.calendar_id = create_app_calendar
|
|
185
|
+
else
|
|
186
|
+
Rails.logger.error("Google Calendar get_calendar error for user #{@user.id}: #{e.class} #{e.status_code} - #{e.message}")
|
|
187
|
+
raise
|
|
188
|
+
end
|
|
189
|
+
rescue Google::Apis::AuthorizationError => e
|
|
190
|
+
Rails.logger.error("Google Calendar get_calendar auth error for user #{@user.id}: #{e.message}")
|
|
191
|
+
raise GoogleCalendarError, I18n.t("collavre.google_calendar.errors.reconnect")
|
|
166
192
|
end
|
|
167
193
|
end
|
|
168
194
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require "base64"
|
|
2
2
|
require "securerandom"
|
|
3
3
|
require "nokogiri"
|
|
4
|
+
require "commonmarker"
|
|
4
5
|
|
|
5
6
|
module Collavre
|
|
6
7
|
# Converts between Markdown and HTML for creative descriptions.
|
|
@@ -9,19 +10,20 @@ module Collavre
|
|
|
9
10
|
# outside of view contexts and reusable from services (e.g. MarkdownImporter).
|
|
10
11
|
class MarkdownConverter
|
|
11
12
|
class << self
|
|
12
|
-
# Convert
|
|
13
|
+
# Convert Markdown to HTML using commonmarker (GFM full support).
|
|
14
|
+
# Falls back to lightweight regex conversion for short inline fragments.
|
|
13
15
|
def markdown_to_html(text, image_refs = {})
|
|
14
16
|
return "" if text.nil?
|
|
15
|
-
|
|
17
|
+
input = text.dup
|
|
16
18
|
|
|
17
19
|
# Collect reference-style data-URI images: [alt]: <data:...>
|
|
18
|
-
|
|
20
|
+
input.gsub!(/^\s*\[([^\]]+)\]:\s*<\s*(data:image\/[^>]+)\s*>\s*$/) do
|
|
19
21
|
image_refs[$1] = $2.strip
|
|
20
22
|
""
|
|
21
23
|
end
|
|
22
24
|
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
+
# Convert data-URI images to Active Storage before rendering
|
|
26
|
+
input.gsub!(/(?<!\\)!\[([^\]]*)\]\[([^\]]+)\]/) do
|
|
25
27
|
if (data_url = image_refs[$2])
|
|
26
28
|
data_image_to_attachment(data_url, $1)
|
|
27
29
|
else
|
|
@@ -29,24 +31,17 @@ module Collavre
|
|
|
29
31
|
end
|
|
30
32
|
end
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
html.gsub!(/(?<!\\)!\[([^\]]*)\]\((data:image\/[^)]+)\)/) do
|
|
34
|
+
input.gsub!(/(?<!\\)!\[([^\]]*)\]\((data:image\/[^)]+)\)/) do
|
|
34
35
|
data_image_to_attachment($2, $1)
|
|
35
36
|
end
|
|
36
37
|
|
|
37
|
-
#
|
|
38
|
-
html.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
html.gsub!(/(?<!\\)(\*\*|__)(.+?)\1/m) do
|
|
44
|
-
"<strong>#{$2}</strong>"
|
|
45
|
-
end
|
|
38
|
+
# Render with commonmarker (GFM extensions: table, strikethrough, autolink, tasklist, tagfilter)
|
|
39
|
+
html = Commonmarker.to_html(input, options: {
|
|
40
|
+
parse: { smart: true },
|
|
41
|
+
render: { unsafe: true },
|
|
42
|
+
extension: { table: true, strikethrough: true, autolink: true, tasklist: true, tagfilter: true }
|
|
43
|
+
})
|
|
46
44
|
|
|
47
|
-
# Unescape backslash-escaped chars
|
|
48
|
-
html.gsub!(/\\([\\*_\[\]()!#~+\-])/, '\\1')
|
|
49
|
-
html.gsub!(/\\\\/, "\\")
|
|
50
45
|
html.strip!
|
|
51
46
|
html
|
|
52
47
|
end
|
|
@@ -92,7 +87,7 @@ module Collavre
|
|
|
92
87
|
token = "__IMG#{index}__"; index += 1
|
|
93
88
|
placeholders[token] = ""
|
|
94
89
|
token
|
|
95
|
-
rescue
|
|
90
|
+
rescue StandardError
|
|
96
91
|
match
|
|
97
92
|
end
|
|
98
93
|
else
|
|
@@ -39,7 +39,7 @@ module Collavre
|
|
|
39
39
|
result: result
|
|
40
40
|
}
|
|
41
41
|
)
|
|
42
|
-
rescue => e
|
|
42
|
+
rescue StandardError => e
|
|
43
43
|
Rails.logger.error("Failed to log tool activity: #{e.message}")
|
|
44
44
|
end
|
|
45
45
|
|
|
@@ -48,14 +48,14 @@ module Collavre
|
|
|
48
48
|
before_call: before_call,
|
|
49
49
|
after_call: after_call
|
|
50
50
|
)
|
|
51
|
-
|
|
51
|
+
Rails.logger.info("Registered tool: #{result}")
|
|
52
52
|
|
|
53
53
|
if result[:error]
|
|
54
54
|
error_msg = "Failed to register tool: #{result[:error]}"
|
|
55
55
|
Rails.logger.error(error_msg)
|
|
56
56
|
raise error_msg
|
|
57
57
|
end
|
|
58
|
-
rescue => e
|
|
58
|
+
rescue StandardError => e
|
|
59
59
|
Rails.logger.error("Failed to register tool from source: #{e.message}")
|
|
60
60
|
raise e
|
|
61
61
|
end
|
|
@@ -131,7 +131,7 @@ module Collavre
|
|
|
131
131
|
if result[:error]
|
|
132
132
|
Rails.logger.error("Failed to delete tool #{tool_name}: #{result[:error]}")
|
|
133
133
|
end
|
|
134
|
-
rescue => e
|
|
134
|
+
rescue StandardError => e
|
|
135
135
|
Rails.logger.error("Failed to delete tool #{tool_name}: #{e.message}")
|
|
136
136
|
end
|
|
137
137
|
|
|
@@ -59,8 +59,8 @@ module Collavre
|
|
|
59
59
|
return unless creative_id && context&.key?("topic")
|
|
60
60
|
|
|
61
61
|
topic_id = context.dig("topic", "id")
|
|
62
|
-
scope = Comment
|
|
63
|
-
.where(creative_id: creative_id, topic_id: topic_id
|
|
62
|
+
scope = Comment.public_only
|
|
63
|
+
.where(creative_id: creative_id, topic_id: topic_id)
|
|
64
64
|
.where.not(user_id: [ task.agent_id, nil ])
|
|
65
65
|
.order(created_at: :desc)
|
|
66
66
|
latest_comment = scope.first
|
|
@@ -99,7 +99,11 @@ module Collavre
|
|
|
99
99
|
return { error: "Creative not found", id: id } unless creative
|
|
100
100
|
return { error: "No write permission on this Creative", id: id } unless creative.has_permission?(Current.user, :write)
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
result = Creatives::DestroyService.new(
|
|
103
|
+
creative: creative,
|
|
104
|
+
user: Current.user
|
|
105
|
+
).call
|
|
106
|
+
return { error: "Failed to destroy creative", id: id } unless result
|
|
103
107
|
{ success: true, id: id, deleted: true }
|
|
104
108
|
end
|
|
105
109
|
end
|
|
@@ -10,7 +10,7 @@ module Tools
|
|
|
10
10
|
tool_description "Create a new recurring scheduled job. The job will periodically post a message to a creative's topic, triggering agent orchestration. Schedule uses cron syntax (e.g., '*/5 * * * *' for every 5 minutes, '0 9 * * *' for daily at 9am)."
|
|
11
11
|
|
|
12
12
|
tool_param :creative_id, description: "The creative ID to post recurring messages to.", required: true
|
|
13
|
-
tool_param :topic_name, description: "The topic name within the creative to post to. Use 'Main' for the main topic
|
|
13
|
+
tool_param :topic_name, description: "The topic name within the creative to post to. Use 'Main' for the default main topic.", required: true
|
|
14
14
|
tool_param :schedule, description: "Cron schedule expression (e.g., '0 9 * * *' for daily at 9am, '*/30 * * * *' for every 30 minutes).", required: true
|
|
15
15
|
tool_param :message, description: "The message content to post on each execution. This triggers the agent orchestration pipeline.", required: true
|
|
16
16
|
tool_param :description, description: "Human-readable description of what this cron job does.", required: false
|
|
@@ -73,12 +73,15 @@ module Tools
|
|
|
73
73
|
private
|
|
74
74
|
|
|
75
75
|
def resolve_topic_id(creative, topic_name)
|
|
76
|
-
|
|
76
|
+
origin = creative.effective_origin
|
|
77
|
+
if topic_name.casecmp(Creative::MAIN_TOPIC_NAME).zero?
|
|
78
|
+
origin.main_topic(fallback_user: Current.user).id
|
|
79
|
+
else
|
|
80
|
+
topic = Topic.find_by(name: topic_name, creative_id: origin.id)
|
|
81
|
+
return { error: "Topic '#{topic_name}' not found for this creative" } unless topic
|
|
77
82
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
topic.id
|
|
83
|
+
topic.id
|
|
84
|
+
end
|
|
82
85
|
end
|
|
83
86
|
end
|
|
84
87
|
end
|