collavre 0.3.0 → 0.3.1
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/controllers/collavre/admin/orchestration_controller.rb +1 -1
- data/app/controllers/collavre/comments_controller.rb +7 -1
- data/app/models/collavre/comment.rb +15 -15
- data/app/services/collavre/comments/topic_command.rb +30 -35
- data/app/services/collavre/orchestration/agent_orchestrator.rb +18 -4
- data/app/services/collavre/orchestration/policy_resolver.rb +1 -1
- data/app/services/collavre/orchestration/stuck_detector.rb +29 -1
- data/app/services/collavre/system_events/context_builder.rb +2 -7
- data/app/views/admin/shared/_tabs.html.erb +1 -1
- data/app/views/collavre/admin/orchestration/show.html.erb +1 -1
- data/config/locales/ai_agent.en.yml +1 -1
- data/config/locales/ai_agent.ko.yml +1 -1
- data/config/locales/comments.en.yml +2 -1
- data/config/locales/comments.ko.yml +2 -1
- data/config/routes.rb +5 -0
- data/lib/collavre/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6e8aa662b4715665ebaa090bc8975037ca96c4059d53acfa379ccc88217487f8
|
|
4
|
+
data.tar.gz: 2d61c18b32e5228bbe84cd9cf8fb73898eedab099ce11959badf95194fb0b431
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3b0baedb45c15a38689e9db2d68bec4fb1ec8cecc7fedfc4a213236bba81e5c65af9eaf54db14b9916811060216856aefaa327ecaa650a8a1ae7ed6a212b74bf
|
|
7
|
+
data.tar.gz: c667c0457625a3c8d0bd7fcf0dd55f9388e6f01ddf23bc3c6f3741bef5abadf3be10031bec72df99851d4aea24815fbb8d7f9caa00d94378f02c0f3790da0cf9
|
|
@@ -17,7 +17,7 @@ module Collavre
|
|
|
17
17
|
validate_policies!(parsed)
|
|
18
18
|
apply_policies!(parsed)
|
|
19
19
|
|
|
20
|
-
redirect_to
|
|
20
|
+
redirect_to admin_orchestration_path, notice: t("admin.orchestration.updated")
|
|
21
21
|
rescue Psych::SyntaxError => e
|
|
22
22
|
flash.now[:alert] = t("admin.orchestration.yaml_syntax_error", message: e.message)
|
|
23
23
|
@policies_yaml = yaml_content
|
|
@@ -179,7 +179,7 @@ module Collavre
|
|
|
179
179
|
chat: {
|
|
180
180
|
content: @comment.content
|
|
181
181
|
}
|
|
182
|
-
}) unless @comment.private?
|
|
182
|
+
}) unless @comment.private? || response.present?
|
|
183
183
|
@comment = Comment.with_attached_images.includes(:comment_reactions).find(@comment.id)
|
|
184
184
|
render partial: "collavre/comments/comment", locals: { comment: @comment, current_topic_id: current_topic_context }, status: :created
|
|
185
185
|
else
|
|
@@ -491,6 +491,12 @@ module Collavre
|
|
|
491
491
|
aliases: [ "/cal" ],
|
|
492
492
|
description: I18n.t("collavre.comments.command_menu.calendar_description"),
|
|
493
493
|
args: I18n.t("collavre.comments.command_menu.calendar_args")
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
name: "topic",
|
|
497
|
+
label: "/topic",
|
|
498
|
+
description: I18n.t("collavre.comments.command_menu.topic_description"),
|
|
499
|
+
args: I18n.t("collavre.comments.command_menu.topic_args")
|
|
494
500
|
}
|
|
495
501
|
] + mcp_command_items
|
|
496
502
|
end
|
|
@@ -75,10 +75,24 @@ module Collavre
|
|
|
75
75
|
:ok
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
def mentioned_users
|
|
79
|
+
return Collavre.user_class.none unless user
|
|
80
|
+
emails = mentioned_emails - [ user.email.downcase ]
|
|
81
|
+
names = mentioned_names - [ user.name.downcase ]
|
|
82
|
+
|
|
83
|
+
origin = creative.effective_origin
|
|
84
|
+
mentionable_users = Collavre.user_class.mentionable_for(origin)
|
|
85
|
+
|
|
86
|
+
scope = Collavre.user_class.none
|
|
87
|
+
scope = scope.or(mentionable_users.where(email: emails)) if emails.any?
|
|
88
|
+
scope = scope.or(mentionable_users.where("LOWER(name) IN (?)", names)) if names.any?
|
|
89
|
+
scope
|
|
90
|
+
end
|
|
91
|
+
|
|
78
92
|
private
|
|
79
93
|
|
|
80
94
|
def cancel_pending_tasks
|
|
81
|
-
Task.where(status: %w[pending running]).each do |task|
|
|
95
|
+
Task.where(status: %w[pending running queued]).each do |task|
|
|
82
96
|
if task.trigger_event_payload&.dig("comment", "id") == id
|
|
83
97
|
task.update!(status: "cancelled")
|
|
84
98
|
end
|
|
@@ -126,20 +140,6 @@ module Collavre
|
|
|
126
140
|
.uniq
|
|
127
141
|
end
|
|
128
142
|
|
|
129
|
-
def mentioned_users
|
|
130
|
-
return Collavre.user_class.none unless user
|
|
131
|
-
emails = mentioned_emails - [ user.email.downcase ]
|
|
132
|
-
names = mentioned_names - [ user.name.downcase ]
|
|
133
|
-
|
|
134
|
-
origin = creative.effective_origin
|
|
135
|
-
mentionable_users = Collavre.user_class.mentionable_for(origin)
|
|
136
|
-
|
|
137
|
-
scope = Collavre.user_class.none
|
|
138
|
-
scope = scope.or(mentionable_users.where(email: emails)) if emails.any?
|
|
139
|
-
scope = scope.or(mentionable_users.where("LOWER(name) IN (?)", names)) if names.any?
|
|
140
|
-
scope
|
|
141
|
-
end
|
|
142
|
-
|
|
143
143
|
def broadcast_create
|
|
144
144
|
return if private?
|
|
145
145
|
broadcast_append_later_to([ creative, :comments ], target: "comments-list", partial: "collavre/comments/comment")
|
|
@@ -34,19 +34,12 @@ module Collavre
|
|
|
34
34
|
content = comment.content.to_s.strip
|
|
35
35
|
|
|
36
36
|
# Extract topic name in quotes
|
|
37
|
-
name_match = content.match(/[""]([^""]+)[""]|"([^"]+)"/)
|
|
37
|
+
name_match = content.match(/[\u201c\u201d""]([^"\u201c\u201d""]+)[\u201c\u201d""]|"([^"]+)"/)
|
|
38
38
|
topic_name = name_match ? (name_match[1] || name_match[2]) : nil
|
|
39
39
|
|
|
40
40
|
return if topic_name.blank?
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
agent_match = content.match(/@(\w+)/)
|
|
44
|
-
agent_name = agent_match ? agent_match[1] : nil
|
|
45
|
-
|
|
46
|
-
{
|
|
47
|
-
name: topic_name,
|
|
48
|
-
agent_name: agent_name
|
|
49
|
-
}
|
|
42
|
+
{ name: topic_name }
|
|
50
43
|
end
|
|
51
44
|
end
|
|
52
45
|
|
|
@@ -54,45 +47,47 @@ module Collavre
|
|
|
54
47
|
data = parsed_args
|
|
55
48
|
return I18n.t("collavre.comments.topic_command.missing_name") if data.blank?
|
|
56
49
|
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
creative: creative,
|
|
60
|
-
user: user,
|
|
61
|
-
name: data[:name]
|
|
62
|
-
)
|
|
50
|
+
# Find primary agent from @mentions using the same parsing as chat
|
|
51
|
+
primary_agent = comment.mentioned_users.find(&:ai_user?)
|
|
63
52
|
|
|
64
|
-
#
|
|
65
|
-
|
|
53
|
+
# Find existing topic or create new one
|
|
54
|
+
existing_topic = Topic.find_by(creative: creative, name: data[:name])
|
|
66
55
|
|
|
67
|
-
if
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
56
|
+
if existing_topic
|
|
57
|
+
if primary_agent
|
|
58
|
+
set_primary_agent(existing_topic, primary_agent)
|
|
59
|
+
I18n.t("collavre.comments.topic_command.updated_agent",
|
|
60
|
+
name: existing_topic.name,
|
|
61
|
+
agent: primary_agent.name)
|
|
62
|
+
else
|
|
63
|
+
I18n.t("collavre.comments.topic_command.already_exists",
|
|
64
|
+
name: existing_topic.name)
|
|
65
|
+
end
|
|
72
66
|
else
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
topic = Topic.create!(
|
|
68
|
+
creative: creative,
|
|
69
|
+
user: user,
|
|
70
|
+
name: data[:name]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if primary_agent
|
|
74
|
+
set_primary_agent(topic, primary_agent)
|
|
75
|
+
I18n.t("collavre.comments.topic_command.created_with_agent",
|
|
75
76
|
name: topic.name,
|
|
76
|
-
|
|
77
|
+
agent: primary_agent.name)
|
|
77
78
|
else
|
|
78
79
|
I18n.t("collavre.comments.topic_command.created", name: topic.name)
|
|
79
80
|
end
|
|
80
81
|
end
|
|
81
82
|
end
|
|
82
83
|
|
|
83
|
-
def find_agent(name)
|
|
84
|
-
# Find AI agent by name (case-insensitive)
|
|
85
|
-
user_class = Collavre.configuration.user_class_name.constantize
|
|
86
|
-
user_class.where.not(llm_vendor: [ nil, "" ])
|
|
87
|
-
.where("LOWER(name) = ?", name.downcase)
|
|
88
|
-
.first
|
|
89
|
-
end
|
|
90
|
-
|
|
91
84
|
def set_primary_agent(topic, agent)
|
|
92
|
-
OrchestratorPolicy.
|
|
85
|
+
policy = OrchestratorPolicy.find_or_initialize_by(
|
|
93
86
|
policy_type: "arbitration",
|
|
94
87
|
scope_type: "Topic",
|
|
95
|
-
scope_id: topic.id
|
|
88
|
+
scope_id: topic.id
|
|
89
|
+
)
|
|
90
|
+
policy.update!(
|
|
96
91
|
config: {
|
|
97
92
|
"strategy" => "primary_first",
|
|
98
93
|
"primary_agent_id" => agent.id
|
|
@@ -24,23 +24,37 @@ module Collavre
|
|
|
24
24
|
if updated > 0
|
|
25
25
|
task.reload
|
|
26
26
|
refresh_deferred_context!(task)
|
|
27
|
-
|
|
27
|
+
|
|
28
|
+
if task.status == "cancelled"
|
|
29
|
+
# refresh_deferred_context! cancelled this task (no eligible comment),
|
|
30
|
+
# try the next queued task for this topic.
|
|
31
|
+
dequeue_next_for_topic(topic_id)
|
|
32
|
+
else
|
|
33
|
+
AiAgentJob.perform_later(task)
|
|
34
|
+
end
|
|
28
35
|
end
|
|
29
36
|
end
|
|
30
37
|
|
|
31
38
|
# Refresh trigger_event_payload so the deferred agent sees the latest
|
|
32
39
|
# conversation state instead of the stale snapshot from enqueue time.
|
|
40
|
+
# Skips AI agent's own comments to prevent self-response loops.
|
|
41
|
+
# Cancels the task if no eligible comment remains.
|
|
33
42
|
def self.refresh_deferred_context!(task)
|
|
34
43
|
context = task.trigger_event_payload
|
|
35
44
|
creative_id = context&.dig("creative", "id")
|
|
36
45
|
return unless creative_id && context&.key?("topic")
|
|
37
46
|
|
|
38
47
|
topic_id = context.dig("topic", "id")
|
|
39
|
-
|
|
48
|
+
scope = Comment
|
|
40
49
|
.where(creative_id: creative_id, topic_id: topic_id, private: false)
|
|
50
|
+
.where.not(user_id: task.agent_id)
|
|
41
51
|
.order(created_at: :desc)
|
|
42
|
-
|
|
43
|
-
|
|
52
|
+
latest_comment = scope.first
|
|
53
|
+
|
|
54
|
+
unless latest_comment
|
|
55
|
+
task.update!(status: "cancelled")
|
|
56
|
+
return
|
|
57
|
+
end
|
|
44
58
|
|
|
45
59
|
context["comment"] = {
|
|
46
60
|
"id" => latest_comment.id,
|
|
@@ -43,7 +43,7 @@ module Collavre
|
|
|
43
43
|
"token_spike_window_minutes" => 10
|
|
44
44
|
},
|
|
45
45
|
"stuck_detection" => {
|
|
46
|
-
"enabled" =>
|
|
46
|
+
"enabled" => true,
|
|
47
47
|
"task_stuck_threshold_minutes" => 30, # Task running for > N minutes
|
|
48
48
|
"creative_stall_threshold_minutes" => 120, # Creative no progress for > N minutes
|
|
49
49
|
"create_system_comment" => true # Create system comment on escalation
|
|
@@ -24,7 +24,7 @@ module Collavre
|
|
|
24
24
|
@policy_resolver = policy_resolver || PolicyResolver.new({})
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
# Run detection and escalation
|
|
27
|
+
# Run detection, auto-recovery, and escalation
|
|
28
28
|
# Returns Result with stuck items and escalation count
|
|
29
29
|
def detect_and_escalate
|
|
30
30
|
config = stuck_detection_config
|
|
@@ -34,6 +34,7 @@ module Collavre
|
|
|
34
34
|
stuck_items.concat(detect_stuck_tasks(config))
|
|
35
35
|
stuck_items.concat(detect_stalled_creatives(config))
|
|
36
36
|
|
|
37
|
+
auto_recover_stuck_tasks(stuck_items)
|
|
37
38
|
escalated_count = escalate_stuck_items(stuck_items, config)
|
|
38
39
|
|
|
39
40
|
Result.new(stuck_items: stuck_items, escalated_count: escalated_count)
|
|
@@ -52,6 +53,33 @@ module Collavre
|
|
|
52
53
|
|
|
53
54
|
private
|
|
54
55
|
|
|
56
|
+
# Auto-recover stuck tasks by marking them as failed and draining the queue.
|
|
57
|
+
def auto_recover_stuck_tasks(stuck_items)
|
|
58
|
+
stuck_items.each do |stuck_item|
|
|
59
|
+
next unless stuck_item.type == :task
|
|
60
|
+
|
|
61
|
+
task = stuck_item.item
|
|
62
|
+
next unless task.status == "running"
|
|
63
|
+
|
|
64
|
+
task.update!(status: "failed")
|
|
65
|
+
Rails.logger.info(
|
|
66
|
+
"[StuckDetector] Auto-recovered task #{task.id} (agent=#{task.agent_id}): " \
|
|
67
|
+
"marked as failed after #{((Time.current - stuck_item.stuck_since) / 60).round} minutes"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Release resources held by the stuck task
|
|
71
|
+
if task.agent
|
|
72
|
+
tracker = ResourceTracker.for(task.agent)
|
|
73
|
+
tracker.release!(task.id)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Drain the queue for the topic so waiting tasks can execute
|
|
77
|
+
AgentOrchestrator.dequeue_next_for_topic(task.topic_id)
|
|
78
|
+
rescue StandardError => e
|
|
79
|
+
Rails.logger.error("[StuckDetector] Auto-recovery failed for task #{task.id}: #{e.message}")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
55
83
|
def stuck_detection_config
|
|
56
84
|
@policy_resolver.resolve("stuck_detection")
|
|
57
85
|
end
|
|
@@ -52,16 +52,11 @@ module Collavre
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def mentioned_user(chat_context)
|
|
55
|
-
# This mimics the chat.mentioned_user function requested
|
|
56
|
-
# It assumes chat_context has 'content' or similar, or we might need to look up the comment
|
|
57
|
-
# For now, let's assume the context passed in already has the necessary info or we extract it.
|
|
58
|
-
# If the event is comment_created, the payload usually has the comment content.
|
|
59
|
-
|
|
60
55
|
content = chat_context["content"]
|
|
61
56
|
return nil unless content
|
|
62
57
|
|
|
63
|
-
#
|
|
64
|
-
match = content.match(/\A@([^:]+?):\s*/)
|
|
58
|
+
# Canonical mention format: @name: (with colon)
|
|
59
|
+
match = content.match(/\A@([^:]+?):\s*/)
|
|
65
60
|
return nil unless match
|
|
66
61
|
|
|
67
62
|
name = match[1].strip
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="tab-list">
|
|
2
2
|
<%= link_to t('admin.tabs.system'), main_app.admin_path, class: "tab-button #{'active' if controller_name == 'settings'}" %>
|
|
3
3
|
<%= link_to t('admin.tabs.users'), collavre.users_path, class: "tab-button #{'active' if controller_name == 'users'}" %>
|
|
4
|
-
<%= link_to t('admin.tabs.orchestration'),
|
|
4
|
+
<%= link_to t('admin.tabs.orchestration'), collavre.admin_orchestration_path, class: "tab-button #{'active' if controller_name == 'orchestration'}" %>
|
|
5
5
|
</div>
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
</p>
|
|
15
15
|
</div>
|
|
16
16
|
|
|
17
|
-
<%= form_with url:
|
|
17
|
+
<%= form_with url: admin_orchestration_path, method: :patch, local: true, html: { class: 'profile-form' } do |f| %>
|
|
18
18
|
<div>
|
|
19
19
|
<label for="policies_yaml" style="font-weight: 600; display: block; margin-bottom: 0.5em;">
|
|
20
20
|
<%= t('admin.orchestration.policies_yaml') %>
|
|
@@ -30,7 +30,7 @@ en:
|
|
|
30
30
|
reviewer_header: "### Reviewers (request review)"
|
|
31
31
|
reference_header: "### References (information only)"
|
|
32
32
|
rules_header: "## Collaboration Rules"
|
|
33
|
-
mention_rule: "- Call other agents: @name request"
|
|
33
|
+
mention_rule: "- Call other agents: @name: request"
|
|
34
34
|
confidence_rule: "- Re-evaluate before responding if uncertain"
|
|
35
35
|
escalation_rule: "- Ask escalation targets for help when stuck"
|
|
36
36
|
review_rule: "- Request review from reviewers when code review is needed"
|
|
@@ -30,7 +30,7 @@ ko:
|
|
|
30
30
|
reviewer_header: "### 리뷰어 (검토 요청)"
|
|
31
31
|
reference_header: "### 참조 (정보 요청만)"
|
|
32
32
|
rules_header: "## 협업 규칙"
|
|
33
|
-
mention_rule: "- 다른 Agent 호출:
|
|
33
|
+
mention_rule: "- 다른 Agent 호출: @이름: 요청내용"
|
|
34
34
|
confidence_rule: "- 확신이 낮으면 재검토 후 발화"
|
|
35
35
|
escalation_rule: "- 막히면 에스컬레이션 대상에게 도움 요청"
|
|
36
36
|
review_rule: "- 코드 리뷰가 필요하면 리뷰어에게 요청"
|
|
@@ -91,7 +91,8 @@ en:
|
|
|
91
91
|
missing_name: 'Please specify a topic name in quotes: /topic "topic name"'
|
|
92
92
|
created: 'Topic "%{name}" created.'
|
|
93
93
|
created_with_agent: 'Topic "%{name}" created with @%{agent} as primary agent.'
|
|
94
|
-
|
|
94
|
+
updated_agent: 'Topic "%{name}" primary agent updated to @%{agent}.'
|
|
95
|
+
already_exists: 'Topic "%{name}" already exists.'
|
|
95
96
|
read_by: Read by %{name}
|
|
96
97
|
activity_logs_summary: Activity Logs
|
|
97
98
|
calendar_events:
|
|
@@ -88,7 +88,8 @@ ko:
|
|
|
88
88
|
missing_name: '토픽 이름을 따옴표로 지정하세요: /topic "토픽 이름"'
|
|
89
89
|
created: '토픽 "%{name}"이(가) 생성되었습니다.'
|
|
90
90
|
created_with_agent: '토픽 "%{name}"이(가) 생성되었습니다. @%{agent}이(가) Primary Agent로 설정되었습니다.'
|
|
91
|
-
|
|
91
|
+
updated_agent: '토픽 "%{name}"의 Primary Agent가 @%{agent}(으)로 변경되었습니다.'
|
|
92
|
+
already_exists: '토픽 "%{name}"이(가) 이미 존재합니다.'
|
|
92
93
|
read_by: "%{name} 님이 읽음"
|
|
93
94
|
activity_logs_summary: 활동 기록
|
|
94
95
|
calendar_events:
|
data/config/routes.rb
CHANGED
|
@@ -93,4 +93,9 @@ Collavre::Engine.routes.draw do
|
|
|
93
93
|
|
|
94
94
|
post "/creative_expanded_states/toggle", to: "creative_expanded_states#toggle"
|
|
95
95
|
post "/comment_read_pointers/update", to: "comment_read_pointers#update"
|
|
96
|
+
|
|
97
|
+
# Admin orchestration
|
|
98
|
+
scope "/admin", as: :admin do
|
|
99
|
+
resource :orchestration, only: [ :show, :update ], controller: "admin/orchestration"
|
|
100
|
+
end
|
|
96
101
|
end
|
data/lib/collavre/version.rb
CHANGED