collavre 0.5.0 → 0.7.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/comment_versions.css +76 -0
- data/app/assets/stylesheets/collavre/comments_popup.css +347 -37
- data/app/assets/stylesheets/collavre/creatives.css +73 -1
- data/app/assets/stylesheets/collavre/org_chart.css +319 -0
- data/app/assets/stylesheets/collavre/popup.css +68 -1
- data/app/controllers/collavre/application_controller.rb +13 -0
- data/app/controllers/collavre/comments/versions_controller.rb +82 -0
- data/app/controllers/collavre/comments_controller.rb +14 -153
- data/app/controllers/collavre/concerns/exportable.rb +30 -0
- data/app/controllers/collavre/concerns/shareable.rb +28 -0
- data/app/controllers/collavre/concerns/slide_viewable.rb +37 -0
- data/app/controllers/collavre/concerns/tree_manageable.rb +141 -0
- data/app/controllers/collavre/creative_imports_controller.rb +6 -0
- data/app/controllers/collavre/creative_invitations_controller.rb +46 -0
- data/app/controllers/collavre/creative_plans_controller.rb +1 -1
- data/app/controllers/collavre/creative_shares_controller.rb +84 -14
- data/app/controllers/collavre/creatives_controller.rb +70 -194
- data/app/controllers/collavre/google_auth_controller.rb +3 -0
- data/app/controllers/collavre/invites_controller.rb +2 -1
- data/app/controllers/collavre/sessions_controller.rb +3 -0
- data/app/controllers/collavre/topics_controller.rb +39 -2
- data/app/controllers/collavre/users_controller.rb +5 -404
- data/app/controllers/concerns/collavre/comments/approval_actions.rb +108 -0
- data/app/controllers/concerns/collavre/comments/batch_operations.rb +55 -0
- data/app/controllers/concerns/collavre/comments/conversion.rb +46 -0
- data/app/controllers/concerns/collavre/users_controller/admin_operations.rb +74 -0
- data/app/controllers/concerns/collavre/users_controller/ai_user_management.rb +119 -0
- data/app/controllers/concerns/collavre/users_controller/contact_management.rb +166 -0
- data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +102 -0
- data/app/controllers/concerns/collavre/users_controller/registration.rb +63 -0
- data/app/helpers/collavre/application_helper.rb +1 -0
- data/app/helpers/collavre/creatives_helper.rb +12 -9
- data/app/helpers/collavre/navigation_helper.rb +1 -1
- data/app/javascript/collavre.js +0 -1
- data/app/javascript/controllers/comment_controller.js +33 -70
- data/app/javascript/controllers/comment_version_controller.js +164 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_review.test.js +305 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_selection.test.js +103 -0
- data/app/javascript/controllers/comments/__tests__/review_quotes_store.test.js +113 -0
- data/app/javascript/controllers/comments/contexts_controller.js +363 -0
- data/app/javascript/controllers/comments/form_controller.js +304 -13
- data/app/javascript/controllers/comments/list_controller.js +151 -62
- data/app/javascript/controllers/comments/popup_controller.js +66 -38
- data/app/javascript/controllers/comments/presence_controller.js +2 -10
- data/app/javascript/controllers/comments/review_quotes_store.js +189 -0
- data/app/javascript/controllers/comments/topics_controller.js +34 -10
- data/app/javascript/controllers/index.js +15 -1
- data/app/javascript/controllers/org_chart_controller.js +46 -0
- data/app/javascript/controllers/share_modal_controller.js +369 -0
- data/app/javascript/controllers/topic_search_controller.js +103 -0
- data/app/javascript/creatives/drag_drop/event_handlers.js +42 -1
- data/app/javascript/lib/api/creatives.js +12 -0
- data/app/javascript/lib/api/csrf_fetch.js +35 -0
- data/app/javascript/lib/api/drag_drop.js +17 -0
- data/app/javascript/modules/command_menu.js +40 -0
- data/app/javascript/modules/creative_row_editor.js +88 -0
- data/app/javascript/modules/slide_view.js +2 -1
- data/app/jobs/collavre/ai_agent_job.rb +42 -30
- data/app/jobs/collavre/compress_job.rb +92 -0
- data/app/models/collavre/comment.rb +36 -1
- data/app/models/collavre/comment_version.rb +15 -0
- data/app/models/collavre/creative/describable.rb +1 -1
- data/app/models/collavre/creative.rb +51 -0
- data/app/models/collavre/task.rb +30 -2
- data/app/models/collavre/user.rb +20 -3
- data/app/services/collavre/ai_agent/a2a_dispatcher.rb +68 -0
- data/app/services/collavre/ai_agent/agent_lifecycle_manager.rb +89 -0
- data/app/services/collavre/ai_agent/message_builder.rb +85 -6
- data/app/services/collavre/ai_agent/response_finalizer.rb +97 -0
- data/app/services/collavre/ai_agent/response_streamer.rb +56 -0
- data/app/services/collavre/ai_agent/review_handler.rb +18 -1
- data/app/services/collavre/ai_agent_service.rb +130 -183
- data/app/services/collavre/ai_client.rb +6 -0
- data/app/services/collavre/auto_theme_generator.rb +1 -1
- data/app/services/collavre/command_menu_service.rb +19 -0
- data/app/services/collavre/comments/command_processor.rb +3 -1
- data/app/services/collavre/comments/compress_command.rb +75 -0
- data/app/services/collavre/comments/concerns/workflow_support.rb +115 -0
- data/app/services/collavre/comments/work_command.rb +161 -0
- data/app/services/collavre/comments/workflow_executor.rb +276 -0
- data/app/services/collavre/creatives/plan_tagger.rb +14 -3
- data/app/services/collavre/creatives/tree_formatter.rb +53 -13
- data/app/services/collavre/gemini_parent_recommender.rb +4 -4
- data/app/services/collavre/orchestration/agent_context_builder.rb +1 -3
- data/app/services/collavre/orchestration/agent_orchestrator.rb +15 -4
- data/app/services/collavre/orchestration/policy_resolver.rb +0 -19
- data/app/services/collavre/orchestration/scheduler.rb +3 -2
- data/app/services/collavre/orchestration/stuck_detector.rb +1 -1
- data/app/services/collavre/system_events/dispatcher.rb +9 -0
- data/app/services/collavre/tools/creative_create_service.rb +1 -8
- data/app/services/collavre/tools/creative_import_service.rb +46 -0
- data/app/services/collavre/tools/creative_retrieval_service.rb +157 -96
- data/app/services/collavre/tools/creative_update_service.rb +1 -8
- data/app/services/collavre/tools/cron_list_service.rb +1 -1
- data/app/services/collavre/tools/description_normalizable.rb +16 -0
- data/app/views/collavre/comments/_comment.html.erb +25 -8
- data/app/views/collavre/comments/_comments_popup.html.erb +32 -5
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +13 -0
- data/app/views/collavre/creatives/_share_button.html.erb +4 -1
- data/app/views/collavre/creatives/_share_modal.html.erb +31 -1
- data/app/views/collavre/creatives/index.html.erb +5 -5
- data/app/views/collavre/creatives/slide_view.html.erb +1 -1
- data/app/views/collavre/users/{_contact_management.html.erb → _contact_list.html.erb} +4 -8
- data/app/views/collavre/users/_org_chart.html.erb +68 -0
- data/app/views/collavre/users/_org_chart_node.html.erb +169 -0
- data/app/views/collavre/users/new_ai.html.erb +9 -0
- data/app/views/collavre/users/show.html.erb +32 -8
- data/config/locales/comments.en.yml +57 -2
- data/config/locales/comments.ko.yml +57 -2
- data/config/locales/contacts.en.yml +31 -0
- data/config/locales/contacts.ko.yml +31 -0
- data/config/locales/contexts.en.yml +8 -0
- data/config/locales/contexts.ko.yml +8 -0
- data/config/locales/creatives.en.yml +6 -0
- data/config/locales/creatives.ko.yml +6 -0
- data/config/locales/users.en.yml +1 -0
- data/config/locales/users.ko.yml +1 -0
- data/config/routes.rb +14 -1
- data/db/migrate/20260220072200_add_workflow_fields_to_tasks.rb +12 -0
- data/db/migrate/20260223173533_add_review_type_to_comments.rb +5 -0
- data/db/migrate/20260225065200_create_comment_versions.rb +14 -0
- data/db/migrate/20260225074416_add_selected_version_id_to_comments.rb +7 -0
- data/lib/collavre/version.rb +1 -1
- metadata +47 -10
- data/app/javascript/lib/lexical/__tests__/action_text_attachment_node.test.jsx +0 -91
- data/app/javascript/lib/lexical/action_text_attachment_node.js +0 -459
- data/app/javascript/lib/lexical/dom_attachment_utils.js +0 -66
- data/app/javascript/modules/share_modal.js +0 -76
- data/app/javascript/modules/share_user_popup.js +0 -77
- data/app/services/collavre/orchestration/self_reflection_evaluator.rb +0 -231
- data/app/views/collavre/comments/_presence_avatars.html.erb +0 -8
- data/app/views/collavre/creatives/_delete_button.html.erb +0 -12
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
module Collavre
|
|
2
|
+
# Orchestrates AI agent execution for a task.
|
|
3
|
+
# Delegates specific concerns to specialized service objects:
|
|
4
|
+
# - AgentLifecycleManager: agent status, cancellation, heartbeats
|
|
5
|
+
# - ResponseStreamer: streaming content updates
|
|
6
|
+
# - ResponseFinalizer: comment finalization, review workflow
|
|
7
|
+
# - A2aDispatcher: agent-to-agent event dispatch
|
|
2
8
|
class AiAgentService
|
|
3
|
-
#
|
|
4
|
-
|
|
5
|
-
# Minimum interval (in seconds) between cancellation checks to avoid excessive DB queries
|
|
6
|
-
CANCEL_CHECK_INTERVAL = 1.0
|
|
7
|
-
# Interval (in seconds) between agent_status heartbeats during streaming
|
|
8
|
-
AGENT_STATUS_HEARTBEAT_INTERVAL = 3.0
|
|
9
|
+
# Compatibility alias for constants moved to AgentLifecycleManager
|
|
10
|
+
CANCEL_CHECK_INTERVAL = AiAgent::AgentLifecycleManager::CANCEL_CHECK_INTERVAL
|
|
9
11
|
|
|
10
12
|
def initialize(task)
|
|
11
13
|
@task = task
|
|
@@ -17,94 +19,47 @@ module Collavre
|
|
|
17
19
|
Current.set(user: @agent) do
|
|
18
20
|
log_action("start", { message: "Starting agent execution" })
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
@original_comment =
|
|
22
|
+
# Build context and messages
|
|
23
|
+
@original_comment = find_original_comment
|
|
24
|
+
messages = build_messages
|
|
25
|
+
log_action("prompt_generated", { messages: messages })
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
# Prepare rendering context and prompts
|
|
28
|
+
@creative = find_creative
|
|
29
|
+
rendering_context = prepare_rendering_context
|
|
30
|
+
system_prompt = render_system_prompt(rendering_context)
|
|
26
31
|
|
|
27
|
-
|
|
32
|
+
# Create placeholder comment if needed
|
|
33
|
+
@reply_comment = create_reply_comment_if_needed
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
creative
|
|
34
|
-
rendering_context["creative"] = creative.as_json if creative
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
agent_context = build_agent_context(creative)
|
|
38
|
-
rendering_context.merge!(agent_context)
|
|
39
|
-
|
|
40
|
-
rendered_system_prompt = AiSystemPromptRenderer.new(
|
|
41
|
-
template: @agent.system_prompt,
|
|
42
|
-
context: rendering_context
|
|
43
|
-
).render
|
|
44
|
-
|
|
45
|
-
collaboration_prompt = build_collaboration_prompt(creative)
|
|
46
|
-
rendered_system_prompt = "#{rendered_system_prompt}\n\n#{collaboration_prompt}" if collaboration_prompt.present?
|
|
47
|
-
|
|
48
|
-
@reply_comment = nil
|
|
49
|
-
|
|
50
|
-
if @original_comment
|
|
51
|
-
@reply_comment = @original_comment.creative.comments.create!(
|
|
52
|
-
content: Comment::STREAMING_PLACEHOLDER_CONTENT,
|
|
53
|
-
user: @agent,
|
|
54
|
-
topic_id: @original_comment.topic_id
|
|
55
|
-
)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
@creative = @context.dig("creative", "id") ? Creative.find_by(id: @context["creative"]["id"]) : nil
|
|
59
|
-
|
|
60
|
-
broadcast_agent_status("thinking")
|
|
61
|
-
|
|
62
|
-
client = AiClient.new(
|
|
63
|
-
vendor: @agent.llm_vendor,
|
|
64
|
-
model: @agent.llm_model,
|
|
65
|
-
system_prompt: rendered_system_prompt,
|
|
66
|
-
llm_api_key: @agent.llm_api_key,
|
|
67
|
-
context: {
|
|
68
|
-
creative: @creative,
|
|
69
|
-
user: @agent,
|
|
70
|
-
task: @task,
|
|
71
|
-
comment: @reply_comment || @original_comment
|
|
72
|
-
}
|
|
35
|
+
# Initialize lifecycle manager
|
|
36
|
+
@lifecycle_manager = AiAgent::AgentLifecycleManager.new(
|
|
37
|
+
task: @task,
|
|
38
|
+
agent: @agent,
|
|
39
|
+
creative: @creative
|
|
73
40
|
)
|
|
74
41
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
check_cancelled!
|
|
81
|
-
@response_content += delta
|
|
42
|
+
# Initialize response streamer
|
|
43
|
+
@streamer = AiAgent::ResponseStreamer.new(
|
|
44
|
+
reply_comment: @reply_comment,
|
|
45
|
+
creative: @creative
|
|
46
|
+
)
|
|
82
47
|
|
|
83
|
-
|
|
84
|
-
if @reply_comment && (now - last_broadcast_at) >= STREAM_THROTTLE_INTERVAL
|
|
85
|
-
@reply_comment.update_column(:content, @response_content)
|
|
86
|
-
@reply_comment.broadcast_update_to(
|
|
87
|
-
[ @reply_comment.creative, :comments ],
|
|
88
|
-
partial: "collavre/comments/comment",
|
|
89
|
-
locals: { comment: @reply_comment, streaming: true }
|
|
90
|
-
)
|
|
91
|
-
last_broadcast_at = now
|
|
92
|
-
end
|
|
48
|
+
@lifecycle_manager.broadcast_status("thinking")
|
|
93
49
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
last_heartbeat_at = now
|
|
98
|
-
end
|
|
99
|
-
end
|
|
50
|
+
# Execute AI chat with streaming
|
|
51
|
+
client = build_ai_client(system_prompt)
|
|
52
|
+
stream_response(client, messages)
|
|
100
53
|
|
|
101
|
-
log_action("completion", { response: @
|
|
54
|
+
log_action("completion", { response: @streamer.content })
|
|
102
55
|
|
|
103
|
-
|
|
56
|
+
# Finalize and dispatch (skip A2A for review-flow updates)
|
|
57
|
+
finalized_comment = finalize_response
|
|
58
|
+
dispatch_a2a(finalized_comment) unless @finalizer&.review_flow
|
|
104
59
|
|
|
105
|
-
|
|
60
|
+
@lifecycle_manager.broadcast_status("idle")
|
|
106
61
|
|
|
107
|
-
@
|
|
62
|
+
@streamer.content
|
|
108
63
|
end
|
|
109
64
|
rescue ApprovalPendingError => e
|
|
110
65
|
AiAgent::ApprovalHandler.new(
|
|
@@ -119,132 +74,124 @@ module Collavre
|
|
|
119
74
|
|
|
120
75
|
private
|
|
121
76
|
|
|
122
|
-
def
|
|
123
|
-
|
|
124
|
-
|
|
77
|
+
def find_original_comment
|
|
78
|
+
target_comment_id = @context.dig("comment", "id")
|
|
79
|
+
target_comment_id ? Comment.find_by(id: target_comment_id) : nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def build_messages
|
|
83
|
+
AiAgent::MessageBuilder.new(
|
|
84
|
+
agent: @agent,
|
|
85
|
+
context: @context,
|
|
86
|
+
original_comment: @original_comment
|
|
87
|
+
).build
|
|
88
|
+
end
|
|
125
89
|
|
|
126
|
-
|
|
127
|
-
|
|
90
|
+
def find_creative
|
|
91
|
+
creative_id = @context.dig("creative", "id")
|
|
92
|
+
creative_id ? Creative.find_by(id: creative_id) : nil
|
|
128
93
|
end
|
|
129
94
|
|
|
130
|
-
def
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
@reply_comment.update!(content: @response_content)
|
|
135
|
-
@reply_comment.broadcast_update_to(
|
|
136
|
-
[ @reply_comment.creative, :comments ],
|
|
137
|
-
partial: "collavre/comments/comment",
|
|
138
|
-
locals: { comment: @reply_comment, streaming: false }
|
|
139
|
-
)
|
|
140
|
-
log_action("reply_created", { comment_id: @reply_comment.id, content: @response_content, partial: true })
|
|
141
|
-
reassociate_activity_logs(@original_comment, @reply_comment)
|
|
142
|
-
else
|
|
143
|
-
@reply_comment.destroy!
|
|
144
|
-
end
|
|
95
|
+
def prepare_rendering_context
|
|
96
|
+
rendering_context = @context.dup
|
|
97
|
+
if @creative
|
|
98
|
+
rendering_context["creative"] = @creative.as_json
|
|
145
99
|
end
|
|
146
100
|
|
|
147
|
-
|
|
148
|
-
|
|
101
|
+
agent_context = build_agent_context(@creative)
|
|
102
|
+
rendering_context.merge!(agent_context)
|
|
103
|
+
rendering_context
|
|
149
104
|
end
|
|
150
105
|
|
|
151
|
-
def
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
@reply_comment.update!(content: @response_content)
|
|
163
|
-
@reply_comment.broadcast_update_to(
|
|
164
|
-
[ @reply_comment.creative, :comments ],
|
|
165
|
-
partial: "collavre/comments/comment",
|
|
166
|
-
locals: { comment: @reply_comment, streaming: false }
|
|
167
|
-
)
|
|
168
|
-
log_action("reply_created", { comment_id: @reply_comment.id, content: @response_content })
|
|
169
|
-
reassociate_activity_logs(@original_comment, @reply_comment)
|
|
170
|
-
dispatch_a2a_if_needed
|
|
171
|
-
end
|
|
172
|
-
else
|
|
173
|
-
@reply_comment.destroy!
|
|
174
|
-
end
|
|
175
|
-
elsif @context.dig("comment", "id") && @response_content.present?
|
|
176
|
-
reply_to_comment(@context["comment"]["id"], @response_content)
|
|
106
|
+
def render_system_prompt(rendering_context)
|
|
107
|
+
rendered = AiSystemPromptRenderer.new(
|
|
108
|
+
template: @agent.system_prompt,
|
|
109
|
+
context: rendering_context
|
|
110
|
+
).render
|
|
111
|
+
|
|
112
|
+
collaboration_prompt = build_collaboration_prompt(@creative)
|
|
113
|
+
if collaboration_prompt.present?
|
|
114
|
+
"#{rendered}\n\n#{collaboration_prompt}"
|
|
115
|
+
else
|
|
116
|
+
rendered
|
|
177
117
|
end
|
|
178
118
|
end
|
|
179
119
|
|
|
180
|
-
def
|
|
181
|
-
return unless @
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
agent_name: @agent.display_name,
|
|
188
|
-
task_id: @task.id,
|
|
189
|
-
content: content,
|
|
190
|
-
source_creative_id: @creative.id
|
|
120
|
+
def create_reply_comment_if_needed
|
|
121
|
+
return nil unless @original_comment
|
|
122
|
+
|
|
123
|
+
@original_comment.creative.comments.create!(
|
|
124
|
+
content: Comment::STREAMING_PLACEHOLDER_CONTENT,
|
|
125
|
+
user: @agent,
|
|
126
|
+
topic_id: @original_comment.topic_id
|
|
191
127
|
)
|
|
192
128
|
end
|
|
193
129
|
|
|
194
|
-
def
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
130
|
+
def build_ai_client(system_prompt)
|
|
131
|
+
AiClient.new(
|
|
132
|
+
vendor: @agent.llm_vendor,
|
|
133
|
+
model: @agent.llm_model,
|
|
134
|
+
system_prompt: system_prompt,
|
|
135
|
+
llm_api_key: @agent.llm_api_key,
|
|
136
|
+
context: {
|
|
137
|
+
creative: @creative,
|
|
138
|
+
user: @agent,
|
|
139
|
+
task: @task,
|
|
140
|
+
comment: @reply_comment || @original_comment
|
|
141
|
+
}
|
|
200
142
|
)
|
|
201
143
|
end
|
|
202
144
|
|
|
203
|
-
def
|
|
204
|
-
|
|
205
|
-
|
|
145
|
+
def stream_response(client, messages)
|
|
146
|
+
client.chat(messages, tools: @agent.tools || []) do |delta|
|
|
147
|
+
@lifecycle_manager.check_cancelled!
|
|
148
|
+
@streamer.append(delta)
|
|
149
|
+
@lifecycle_manager.heartbeat_if_needed
|
|
150
|
+
end
|
|
151
|
+
end
|
|
206
152
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
153
|
+
def finalize_response
|
|
154
|
+
@finalizer = AiAgent::ResponseFinalizer.new(
|
|
155
|
+
task: @task,
|
|
156
|
+
agent: @agent,
|
|
157
|
+
original_comment: @original_comment,
|
|
158
|
+
reply_comment: @reply_comment,
|
|
159
|
+
response_content: @streamer.content
|
|
211
160
|
)
|
|
212
|
-
|
|
213
|
-
log_action("reply_created", { comment_id: @reply_comment.id, content: content })
|
|
214
|
-
reassociate_activity_logs(original_comment, @reply_comment)
|
|
215
|
-
dispatch_a2a_if_needed
|
|
161
|
+
@finalizer.finalize
|
|
216
162
|
end
|
|
217
163
|
|
|
218
|
-
def
|
|
219
|
-
return unless
|
|
164
|
+
def dispatch_a2a(finalized_comment)
|
|
165
|
+
return unless finalized_comment
|
|
220
166
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
167
|
+
dispatcher = AiAgent::A2aDispatcher.new(
|
|
168
|
+
agent: @agent,
|
|
169
|
+
reply_comment: finalized_comment,
|
|
170
|
+
context: @context
|
|
171
|
+
)
|
|
172
|
+
dispatcher.dispatch
|
|
173
|
+
end
|
|
224
174
|
|
|
225
|
-
|
|
175
|
+
def handle_cancelled
|
|
176
|
+
@lifecycle_manager.handle_cancelled(
|
|
177
|
+
reply_comment: @reply_comment,
|
|
178
|
+
response_content: @streamer.content
|
|
179
|
+
)
|
|
226
180
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
end
|
|
181
|
+
# Reassociate activity logs if there was partial content
|
|
182
|
+
if @reply_comment && @streamer.content_present?
|
|
183
|
+
ActivityLog.where(comment: @original_comment, user: @agent)
|
|
184
|
+
.update_all(comment_id: @reply_comment.id)
|
|
232
185
|
end
|
|
233
|
-
|
|
234
|
-
# Dispatch event — downstream Matcher will route to the correct agent(s)
|
|
235
|
-
SystemEvents::Dispatcher.dispatch("comment_created", {
|
|
236
|
-
comment: { id: @reply_comment.id, content: @reply_comment.content, user_id: @reply_comment.user_id },
|
|
237
|
-
creative: { id: creative&.id, description: creative&.description },
|
|
238
|
-
topic: { id: @reply_comment.topic_id },
|
|
239
|
-
chat: { content: @reply_comment.content }
|
|
240
|
-
})
|
|
241
|
-
rescue StandardError => e
|
|
242
|
-
Rails.logger.error("[AiAgentService] A2A dispatch failed: #{e.message}")
|
|
243
186
|
end
|
|
244
187
|
|
|
245
|
-
def
|
|
246
|
-
|
|
247
|
-
|
|
188
|
+
def log_action(type, payload, result = nil)
|
|
189
|
+
@task.task_actions.create!(
|
|
190
|
+
action_type: type,
|
|
191
|
+
payload: payload,
|
|
192
|
+
result: result,
|
|
193
|
+
status: "done"
|
|
194
|
+
)
|
|
248
195
|
end
|
|
249
196
|
|
|
250
197
|
def build_agent_context(creative)
|
|
@@ -8,12 +8,16 @@ module Collavre
|
|
|
8
8
|
- Respond in the asker's language (prefer the latest user message). Keep code and error messages in their original form.
|
|
9
9
|
PROMPT
|
|
10
10
|
|
|
11
|
+
attr_reader :last_input_tokens, :last_output_tokens
|
|
12
|
+
|
|
11
13
|
def initialize(vendor:, model:, system_prompt:, llm_api_key: nil, context: {})
|
|
12
14
|
@vendor = vendor
|
|
13
15
|
@model = model
|
|
14
16
|
@system_prompt = system_prompt
|
|
15
17
|
@llm_api_key = llm_api_key
|
|
16
18
|
@context = context
|
|
19
|
+
@last_input_tokens = 0
|
|
20
|
+
@last_output_tokens = 0
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
def chat(contents, tools: [], &block)
|
|
@@ -90,6 +94,8 @@ module Collavre
|
|
|
90
94
|
yield "\n\n⚠️ AI Error: #{error_message}" if block_given?
|
|
91
95
|
nil
|
|
92
96
|
ensure
|
|
97
|
+
@last_input_tokens = input_tokens || 0
|
|
98
|
+
@last_output_tokens = output_tokens || 0
|
|
93
99
|
log_interaction(
|
|
94
100
|
messages: conversation.messages.to_a || Array(contents),
|
|
95
101
|
tools: conversation.tools.to_a,
|
|
@@ -26,6 +26,25 @@ module Collavre
|
|
|
26
26
|
label: "/topic",
|
|
27
27
|
description: I18n.t("collavre.comments.command_menu.topic_description"),
|
|
28
28
|
args: I18n.t("collavre.comments.command_menu.topic_args")
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "work",
|
|
32
|
+
label: "/work",
|
|
33
|
+
description: I18n.t("collavre.comments.command_menu.work_description"),
|
|
34
|
+
args: I18n.t("collavre.comments.command_menu.work_args")
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "compress",
|
|
38
|
+
label: "/compress",
|
|
39
|
+
description: I18n.t("collavre.comments.command_menu.compress_description"),
|
|
40
|
+
args: I18n.t("collavre.comments.command_menu.compress_args")
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "creative",
|
|
44
|
+
label: "/creative",
|
|
45
|
+
type: "popup",
|
|
46
|
+
popup_type: "creative_picker",
|
|
47
|
+
description: I18n.t("collavre.comments.command_menu.creative_description")
|
|
29
48
|
}
|
|
30
49
|
]
|
|
31
50
|
end
|
|
@@ -28,7 +28,9 @@ module Collavre
|
|
|
28
28
|
def static_commands
|
|
29
29
|
[
|
|
30
30
|
Collavre::Comments::CalendarCommand.new(comment: comment, user: user),
|
|
31
|
-
Collavre::Comments::TopicCommand.new(comment: comment, user: user)
|
|
31
|
+
Collavre::Comments::TopicCommand.new(comment: comment, user: user),
|
|
32
|
+
Collavre::Comments::WorkCommand.new(comment: comment, user: user),
|
|
33
|
+
Collavre::Comments::CompressCommand.new(comment: comment, user: user)
|
|
32
34
|
]
|
|
33
35
|
end
|
|
34
36
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
module Comments
|
|
3
|
+
class CompressCommand
|
|
4
|
+
COMMAND_PATTERN = /\A\/compress\b/i.freeze
|
|
5
|
+
|
|
6
|
+
SYSTEM_PROMPT = <<~PROMPT.freeze
|
|
7
|
+
You are summarizing a conversation thread in a project management tool.
|
|
8
|
+
Preserve key decisions, action items, important context, and any conclusions.
|
|
9
|
+
Be concise but thorough. Respond in the same language as the conversation.
|
|
10
|
+
Use markdown formatting for readability.
|
|
11
|
+
PROMPT
|
|
12
|
+
|
|
13
|
+
def initialize(comment:, user:)
|
|
14
|
+
@comment = comment
|
|
15
|
+
@user = user
|
|
16
|
+
@creative = comment.creative
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call
|
|
20
|
+
return unless compress_command?
|
|
21
|
+
|
|
22
|
+
validate!
|
|
23
|
+
authorize!
|
|
24
|
+
enqueue_compress_job
|
|
25
|
+
rescue StandardError => e
|
|
26
|
+
Rails.logger.error("Compress command failed: #{e.message}")
|
|
27
|
+
e.message
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
attr_reader :comment, :user, :creative
|
|
33
|
+
|
|
34
|
+
def compress_command?
|
|
35
|
+
comment.content.to_s.strip.match?(COMMAND_PATTERN)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def extra_prompt
|
|
39
|
+
content = comment.content.to_s.strip
|
|
40
|
+
rest = content.sub(COMMAND_PATTERN, "").strip
|
|
41
|
+
rest.presence
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def validate!
|
|
45
|
+
unless comment.topic_id.present?
|
|
46
|
+
raise I18n.t("collavre.comments.compress_command.topic_required")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The /compress command comment may not be saved yet, so count existing non-compress comments
|
|
50
|
+
compress_pattern = /\A\/compress\b/i
|
|
51
|
+
existing_count = creative.comments.where(topic_id: comment.topic_id)
|
|
52
|
+
.reject { |c| c.content.to_s.strip.match?(compress_pattern) }.size
|
|
53
|
+
if existing_count < 2
|
|
54
|
+
raise I18n.t("collavre.comments.compress_command.nothing_to_compress")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def authorize!
|
|
59
|
+
unless creative.has_permission?(user, :write)
|
|
60
|
+
raise I18n.t("collavre.comments.compress_command.not_authorized")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def enqueue_compress_job
|
|
65
|
+
CompressJob.perform_later(
|
|
66
|
+
creative.id,
|
|
67
|
+
comment.topic_id,
|
|
68
|
+
user.id,
|
|
69
|
+
extra_prompt
|
|
70
|
+
)
|
|
71
|
+
I18n.t("collavre.comments.compress_command.started")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
module Comments
|
|
3
|
+
module Concerns
|
|
4
|
+
module WorkflowSupport
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
# Filter creatives that already have active tasks or are completed
|
|
10
|
+
# Returns array of creative IDs to skip
|
|
11
|
+
def filter_active_or_completed(creative_ids)
|
|
12
|
+
return [] if creative_ids.empty?
|
|
13
|
+
|
|
14
|
+
# Skip creatives with active tasks (done/failed/cancelled allow re-work)
|
|
15
|
+
active = Task.where(creative_id: creative_ids)
|
|
16
|
+
.where(status: %w[running queued pending pending_approval])
|
|
17
|
+
.pluck(:creative_id)
|
|
18
|
+
|
|
19
|
+
# Skip creatives already completed (progress >= 1.0)
|
|
20
|
+
completed = Creative.where(id: creative_ids)
|
|
21
|
+
.where("progress >= 1.0")
|
|
22
|
+
.pluck(:id)
|
|
23
|
+
|
|
24
|
+
(active + completed).uniq
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# DFS traversal of creative tree
|
|
28
|
+
def dfs_traverse(node, &block)
|
|
29
|
+
yield node
|
|
30
|
+
node.children.order(:sequence).each { |child| dfs_traverse(child, &block) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Find the original user who triggered the workflow
|
|
34
|
+
# Looks in: comment author → stored user_id → creative owner → agent
|
|
35
|
+
def find_original_user_from_task(task)
|
|
36
|
+
comment_id = task.trigger_event_payload&.dig("comment", "id")
|
|
37
|
+
comment = Comment.find_by(id: comment_id) if comment_id
|
|
38
|
+
|
|
39
|
+
# Try: comment author → stored user_id → creative owner → agent (last resort)
|
|
40
|
+
user = comment&.user
|
|
41
|
+
user ||= User.find_by(id: task.trigger_event_payload&.dig("comment", "user_id"))
|
|
42
|
+
user ||= task.creative&.user
|
|
43
|
+
user ||= task.agent
|
|
44
|
+
|
|
45
|
+
Rails.logger.warn("[WorkflowSupport] Using fallback user (#{user&.class}:#{user&.id}) for task ##{task.id}") unless comment&.user
|
|
46
|
+
|
|
47
|
+
user
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Post a notice comment to the original creative thread
|
|
51
|
+
def post_workflow_notice(task, content)
|
|
52
|
+
comment_id = task.trigger_event_payload&.dig("comment", "id")
|
|
53
|
+
unless comment_id
|
|
54
|
+
Rails.logger.warn("[WorkflowSupport] post_workflow_notice: no comment_id in trigger_event_payload: #{task.trigger_event_payload.inspect}")
|
|
55
|
+
return
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
original_comment = Comment.find_by(id: comment_id)
|
|
59
|
+
unless original_comment
|
|
60
|
+
Rails.logger.warn("[WorkflowSupport] post_workflow_notice: comment #{comment_id} not found")
|
|
61
|
+
return
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
Rails.logger.info("[WorkflowSupport] post_workflow_notice: posting to creative #{original_comment.creative_id}")
|
|
65
|
+
original_comment.creative.comments.create!(
|
|
66
|
+
content: content,
|
|
67
|
+
user: task.agent,
|
|
68
|
+
topic_id: original_comment.topic_id
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Resolve workflow context: if it's a creative ID, render that creative's markdown
|
|
73
|
+
def resolve_workflow_context_from_task(task, state)
|
|
74
|
+
# Use cached rendered context if available (avoids re-rendering in job context)
|
|
75
|
+
cached = state["rendered_workflow_context"]
|
|
76
|
+
return cached if cached.present?
|
|
77
|
+
|
|
78
|
+
context_text = task.workflow_context.to_s.strip
|
|
79
|
+
creative_id = context_text[/\A\d+\z/]
|
|
80
|
+
|
|
81
|
+
resolved = if creative_id
|
|
82
|
+
context_creative = Creative.find_by(id: creative_id)
|
|
83
|
+
if context_creative
|
|
84
|
+
markdown = render_creative_markdown_for_task(task, context_creative)
|
|
85
|
+
markdown.presence || context_text
|
|
86
|
+
else
|
|
87
|
+
context_text
|
|
88
|
+
end
|
|
89
|
+
else
|
|
90
|
+
context_text
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Cache for subsequent sub-tasks
|
|
94
|
+
state["rendered_workflow_context"] = resolved
|
|
95
|
+
task.update!(workflow_state: state)
|
|
96
|
+
|
|
97
|
+
resolved
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Render creative tree as markdown with agent's depth settings
|
|
101
|
+
def render_creative_markdown_for_task(task, creative)
|
|
102
|
+
max_depth = task.agent.creative_children_level + 1
|
|
103
|
+
result = ApplicationController.helpers.render_creative_tree_markdown(
|
|
104
|
+
[ creative ], 1, true, max_depth: max_depth
|
|
105
|
+
)
|
|
106
|
+
Rails.logger.info("[WorkflowSupport] render_creative_markdown: creative=#{creative.id} result_length=#{result&.length}")
|
|
107
|
+
result
|
|
108
|
+
rescue StandardError => e
|
|
109
|
+
Rails.logger.error("[WorkflowSupport] render_creative_markdown FAILED: creative=#{creative.id} error=#{e.class}: #{e.message}\n#{e.backtrace&.first(5)&.join("\n")}")
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|