collavre_openclaw 0.5.0 → 0.6.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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ff18cc7843b54de3c5e86de861f69b9225aa47dd7e57aae94cd612d7698d1d55
|
|
4
|
+
data.tar.gz: 8b83ffc51769d3946de8660b0583ccc672079f08d80db9b93178c12c1c374279
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 54bf09a092ed87b269621100c55edadd8fd0591f41f25270672f27cf6837d3767dff33a3cff6bb6fe4315c90cbb5d596805b6ce6d2ec055eb5d0cd0f73e7d5d6
|
|
7
|
+
data.tar.gz: b967fc6beb11b41889b0574b937b94532910acba29e9b349e2ec40289fcb57bd9096ea4cf06756a5db05acdc5f9a13a51c1c3474fb8fe85709e1c68fafe703dc
|
|
@@ -12,8 +12,11 @@ module CollavreOpenclaw
|
|
|
12
12
|
end
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
# @param messages_input [Hash, Array] Hash { messages:, first_message:, context_changed: }
|
|
16
|
+
# from MessageBuilder, or a plain Array from standalone callers (e.g., CompressJob).
|
|
17
|
+
def chat(messages_input, tools: [], &block)
|
|
16
18
|
normalized_vendor = vendor.to_s.downcase
|
|
19
|
+
messages_data = normalize_messages_input(messages_input)
|
|
17
20
|
|
|
18
21
|
# Check if we have a custom adapter for this vendor
|
|
19
22
|
adapter_class = self.class.adapter_registry[normalized_vendor]
|
|
@@ -31,13 +34,13 @@ module CollavreOpenclaw
|
|
|
31
34
|
error_message = nil
|
|
32
35
|
|
|
33
36
|
begin
|
|
34
|
-
response_content = adapter.chat(
|
|
37
|
+
response_content = adapter.chat(messages_data, &block)
|
|
35
38
|
rescue StandardError => e
|
|
36
39
|
error_message = e.message
|
|
37
40
|
raise
|
|
38
41
|
ensure
|
|
39
42
|
log_interaction(
|
|
40
|
-
messages:
|
|
43
|
+
messages: messages_data[:messages],
|
|
41
44
|
tools: [],
|
|
42
45
|
response_content: response_content,
|
|
43
46
|
error_message: error_message,
|
|
@@ -49,12 +52,24 @@ module CollavreOpenclaw
|
|
|
49
52
|
return response_content
|
|
50
53
|
end
|
|
51
54
|
|
|
52
|
-
# Fall back to original implementation
|
|
53
|
-
super
|
|
55
|
+
# Fall back to original RubyLLM implementation (expects Array)
|
|
56
|
+
super(messages_data[:messages], tools: tools, &block)
|
|
54
57
|
end
|
|
55
58
|
|
|
56
59
|
private
|
|
57
60
|
|
|
58
61
|
attr_reader :vendor, :system_prompt, :context
|
|
62
|
+
|
|
63
|
+
# Wrap plain Array input (from standalone callers like CompressJob)
|
|
64
|
+
# into the Hash format expected by the adapter.
|
|
65
|
+
def normalize_messages_input(input)
|
|
66
|
+
return input if input.is_a?(Hash)
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
messages: Array(input).map { |m| m.merge(kind: :trigger) },
|
|
70
|
+
first_message: true,
|
|
71
|
+
context_changed: false
|
|
72
|
+
}
|
|
73
|
+
end
|
|
59
74
|
end
|
|
60
75
|
end
|
|
@@ -20,7 +20,10 @@ module CollavreOpenclaw
|
|
|
20
20
|
@context = context
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
# @param messages_data [Hash] { messages:, first_message:, context_changed: }
|
|
24
|
+
def chat(messages_data, &block)
|
|
25
|
+
parse_messages_data!(messages_data)
|
|
26
|
+
|
|
24
27
|
unless @user&.gateway_url.present?
|
|
25
28
|
Rails.logger.error("[CollavreOpenclaw] No Gateway URL configured for user #{@user&.id}")
|
|
26
29
|
yield "Error: OpenClaw Gateway URL not configured" if block_given?
|
|
@@ -38,11 +41,11 @@ module CollavreOpenclaw
|
|
|
38
41
|
# Set OPENCLAW_TRANSPORT=http to force HTTP-only mode
|
|
39
42
|
if CollavreOpenclaw.config.transport == "http"
|
|
40
43
|
Rails.logger.info("[CollavreOpenclaw::WS] TRANSPORT mode=http_forced")
|
|
41
|
-
chat_via_http(
|
|
44
|
+
chat_via_http(&block)
|
|
42
45
|
elsif websocket_available?
|
|
43
|
-
chat_via_websocket(
|
|
46
|
+
chat_via_websocket(&block)
|
|
44
47
|
else
|
|
45
|
-
chat_via_http(
|
|
48
|
+
chat_via_http(&block)
|
|
46
49
|
end
|
|
47
50
|
end
|
|
48
51
|
|
|
@@ -69,6 +72,27 @@ module CollavreOpenclaw
|
|
|
69
72
|
|
|
70
73
|
private
|
|
71
74
|
|
|
75
|
+
CONTEXT_KINDS = %i[creative_context context_creative referenced_creative].freeze
|
|
76
|
+
|
|
77
|
+
def parse_messages_data!(data)
|
|
78
|
+
@all_messages = data[:messages] || []
|
|
79
|
+
@first_message = data[:first_message]
|
|
80
|
+
@context_changed = data[:context_changed]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def context_messages
|
|
84
|
+
@all_messages.select { |m| CONTEXT_KINDS.include?(m[:kind]) }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def trigger_message
|
|
88
|
+
@all_messages.find { |m| m[:kind] == :trigger }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Send system prompt and context on first message or when they changed.
|
|
92
|
+
def include_full_context?
|
|
93
|
+
@first_message || @context_changed
|
|
94
|
+
end
|
|
95
|
+
|
|
72
96
|
# ─────────────────────────────────────────────
|
|
73
97
|
# WebSocket transport
|
|
74
98
|
# ─────────────────────────────────────────────
|
|
@@ -83,14 +107,14 @@ module CollavreOpenclaw
|
|
|
83
107
|
false
|
|
84
108
|
end
|
|
85
109
|
|
|
86
|
-
def chat_via_websocket(
|
|
110
|
+
def chat_via_websocket(&block)
|
|
87
111
|
response_content = +""
|
|
88
112
|
|
|
89
113
|
begin
|
|
90
114
|
client = ConnectionManager.instance.connection_for(@user)
|
|
91
|
-
payload = build_ws_chat_payload
|
|
115
|
+
payload = build_ws_chat_payload
|
|
92
116
|
|
|
93
|
-
Rails.logger.info("[CollavreOpenclaw] Sending via WebSocket (session: #{session_key})")
|
|
117
|
+
Rails.logger.info("[CollavreOpenclaw] Sending via WebSocket (session: #{session_key}, first: #{@first_message}, changed: #{@context_changed})")
|
|
94
118
|
|
|
95
119
|
client.chat_send(
|
|
96
120
|
session_key: session_key,
|
|
@@ -121,7 +145,7 @@ module CollavreOpenclaw
|
|
|
121
145
|
rescue CollavreOpenclaw::ConnectionError,
|
|
122
146
|
CollavreOpenclaw::TimeoutError => e
|
|
123
147
|
Rails.logger.warn("[CollavreOpenclaw::WS] FALLBACK gateway=#{@user.gateway_url} reason=#{e.class}:#{e.message}")
|
|
124
|
-
chat_via_http(
|
|
148
|
+
chat_via_http(&block)
|
|
125
149
|
rescue CollavreOpenclaw::ChatError, CollavreOpenclaw::RpcError => e
|
|
126
150
|
Rails.logger.error("[CollavreOpenclaw] WebSocket chat error: #{e.message}")
|
|
127
151
|
error_msg = "OpenClaw Error: #{e.message}"
|
|
@@ -131,72 +155,46 @@ module CollavreOpenclaw
|
|
|
131
155
|
Rails.logger.error("[CollavreOpenclaw] WebSocket unexpected error: #{e.message}\n" \
|
|
132
156
|
"#{e.backtrace.first(5).join("\n")}")
|
|
133
157
|
Rails.logger.info("[CollavreOpenclaw::WS] FALLBACK gateway=#{@user.gateway_url} reason=#{e.class}:#{e.message}")
|
|
134
|
-
chat_via_http(
|
|
158
|
+
chat_via_http(&block)
|
|
135
159
|
end
|
|
136
160
|
end
|
|
137
161
|
|
|
138
162
|
# Build WebSocket chat.send payload.
|
|
139
163
|
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
|
|
164
|
+
# Token optimization: only includes system prompt and creative context on
|
|
165
|
+
# the first message or when context has changed. The Gateway maintains its
|
|
166
|
+
# own session history, so chat history is never sent — only the trigger.
|
|
167
|
+
def build_ws_chat_payload
|
|
168
|
+
trigger = trigger_message
|
|
143
169
|
{
|
|
144
|
-
message: format_message_for_ws
|
|
145
|
-
attachments: extract_ws_attachments(
|
|
170
|
+
message: format_message_for_ws,
|
|
171
|
+
attachments: trigger ? extract_ws_attachments([ trigger ]).presence : nil
|
|
146
172
|
}
|
|
147
173
|
end
|
|
148
174
|
|
|
149
175
|
# Format messages for WebSocket chat.send text payload.
|
|
150
176
|
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
177
|
+
# On first message (or context change):
|
|
178
|
+
# [system prompt] + [creative context] + [trigger]
|
|
179
|
+
# On subsequent messages:
|
|
180
|
+
# [trigger only]
|
|
155
181
|
#
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
|
|
159
|
-
# [context creative messages]
|
|
160
|
-
# [chat history]
|
|
161
|
-
# [latest user message]
|
|
162
|
-
def format_message_for_ws(messages)
|
|
163
|
-
formatted = Array(messages)
|
|
164
|
-
return "" if formatted.empty?
|
|
165
|
-
|
|
182
|
+
# Chat history is NOT included — the Gateway's SessionManager tracks
|
|
183
|
+
# conversation turns automatically.
|
|
184
|
+
def format_message_for_ws
|
|
166
185
|
parts = []
|
|
167
186
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
role = m[:role] || m["role"]
|
|
174
|
-
next unless role.to_s == "user"
|
|
175
|
-
|
|
176
|
-
text = extract_message_text(m)
|
|
177
|
-
next unless text.present?
|
|
178
|
-
next unless text.match?(/\A(Creative|Context Creative|Referenced Creative)\s*\(/)
|
|
179
|
-
|
|
180
|
-
parts << text
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
# 3. Chat history (prior user/assistant exchanges)
|
|
184
|
-
formatted.each do |m|
|
|
185
|
-
role = (m[:role] || m["role"]).to_s
|
|
186
|
-
text = extract_message_text(m)
|
|
187
|
-
next unless text.present?
|
|
188
|
-
|
|
189
|
-
# Skip context messages (already included above)
|
|
190
|
-
next if text.match?(/\A(Creative|Context Creative|Referenced Creative)\s*\(/)
|
|
191
|
-
|
|
192
|
-
case role
|
|
193
|
-
when "user"
|
|
194
|
-
parts << text
|
|
195
|
-
when "assistant", "model"
|
|
196
|
-
parts << "[Assistant]: #{text}"
|
|
187
|
+
if include_full_context?
|
|
188
|
+
parts << @system_prompt if @system_prompt.present?
|
|
189
|
+
context_messages.each do |m|
|
|
190
|
+
text = extract_message_text(m)
|
|
191
|
+
parts << text if text.present?
|
|
197
192
|
end
|
|
198
193
|
end
|
|
199
194
|
|
|
195
|
+
trigger = trigger_message
|
|
196
|
+
parts << extract_message_text(trigger) if trigger
|
|
197
|
+
|
|
200
198
|
parts.join("\n\n")
|
|
201
199
|
end
|
|
202
200
|
|
|
@@ -297,13 +295,13 @@ module CollavreOpenclaw
|
|
|
297
295
|
# HTTP transport (fallback)
|
|
298
296
|
# ─────────────────────────────────────────────
|
|
299
297
|
|
|
300
|
-
def chat_via_http(
|
|
298
|
+
def chat_via_http(&block)
|
|
301
299
|
response_content = +""
|
|
302
300
|
|
|
303
301
|
begin
|
|
304
|
-
payload = build_payload
|
|
302
|
+
payload = build_payload
|
|
305
303
|
|
|
306
|
-
Rails.logger.info("[CollavreOpenclaw] Sending via HTTP to #{api_endpoint} (session: #{session_key})")
|
|
304
|
+
Rails.logger.info("[CollavreOpenclaw] Sending via HTTP to #{api_endpoint} (session: #{session_key}, first: #{@first_message}, changed: #{@context_changed})")
|
|
307
305
|
|
|
308
306
|
stream_response(payload) do |chunk|
|
|
309
307
|
response_content << chunk
|
|
@@ -349,22 +347,35 @@ module CollavreOpenclaw
|
|
|
349
347
|
# HTTP payload building
|
|
350
348
|
# ─────────────────────────────────────────────
|
|
351
349
|
|
|
352
|
-
|
|
350
|
+
# Build HTTP payload with token optimization.
|
|
351
|
+
#
|
|
352
|
+
# On first message (or context change):
|
|
353
|
+
# system prompt + creative context + trigger
|
|
354
|
+
# On subsequent messages:
|
|
355
|
+
# trigger only
|
|
356
|
+
#
|
|
357
|
+
# Chat history is NOT included — the Gateway's SessionManager tracks
|
|
358
|
+
# conversation turns via the stable session key.
|
|
359
|
+
def build_payload
|
|
353
360
|
agent_id = extract_agent_id_from_email
|
|
354
361
|
model_value = agent_id.present? ? "openclaw:#{agent_id}" : "openclaw"
|
|
355
362
|
|
|
356
|
-
|
|
357
|
-
model: model_value,
|
|
358
|
-
messages: format_messages(messages),
|
|
359
|
-
stream: true
|
|
360
|
-
}
|
|
363
|
+
formatted = []
|
|
361
364
|
|
|
362
|
-
if
|
|
363
|
-
|
|
365
|
+
if include_full_context?
|
|
366
|
+
formatted << { role: "system", content: @system_prompt } if @system_prompt.present?
|
|
367
|
+
context_messages.each { |m| formatted << format_single_message(m) }
|
|
364
368
|
end
|
|
365
369
|
|
|
366
|
-
|
|
367
|
-
|
|
370
|
+
trigger = trigger_message
|
|
371
|
+
formatted << format_single_message(trigger) if trigger
|
|
372
|
+
|
|
373
|
+
{
|
|
374
|
+
model: model_value,
|
|
375
|
+
messages: formatted,
|
|
376
|
+
stream: true,
|
|
377
|
+
user: build_user_context
|
|
378
|
+
}
|
|
368
379
|
end
|
|
369
380
|
|
|
370
381
|
def build_user_context
|
|
@@ -400,28 +411,26 @@ module CollavreOpenclaw
|
|
|
400
411
|
end
|
|
401
412
|
end
|
|
402
413
|
|
|
403
|
-
def
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
text = extract_message_text(msg)
|
|
414
|
+
def format_single_message(msg)
|
|
415
|
+
role = msg[:role] || msg["role"]
|
|
416
|
+
text = extract_message_text(msg)
|
|
407
417
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
418
|
+
sender_name = msg[:sender_name] || msg["sender_name"]
|
|
419
|
+
if sender_name.present? && normalize_role(role) == "user"
|
|
420
|
+
text = "[#{sender_name}]: #{text}"
|
|
421
|
+
end
|
|
412
422
|
|
|
413
|
-
|
|
423
|
+
image_sources = extract_image_sources(msg)
|
|
414
424
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
end
|
|
421
|
-
{ role: normalize_role(role), content: content_parts }
|
|
422
|
-
else
|
|
423
|
-
{ role: normalize_role(role), content: text.to_s }
|
|
425
|
+
if image_sources.any?
|
|
426
|
+
content_parts = [ { type: "text", text: text.to_s } ]
|
|
427
|
+
image_sources.each do |source|
|
|
428
|
+
image_data = encode_image_source(source)
|
|
429
|
+
content_parts << image_data if image_data
|
|
424
430
|
end
|
|
431
|
+
{ role: normalize_role(role), content: content_parts }
|
|
432
|
+
else
|
|
433
|
+
{ role: normalize_role(role), content: text.to_s }
|
|
425
434
|
end
|
|
426
435
|
end
|
|
427
436
|
|
|
@@ -265,6 +265,21 @@ module CollavreOpenclaw
|
|
|
265
265
|
end
|
|
266
266
|
end
|
|
267
267
|
|
|
268
|
+
# Inject an assistant message into a session transcript.
|
|
269
|
+
# Useful for pre-populating context without triggering an agent run.
|
|
270
|
+
#
|
|
271
|
+
# @param session_key [String]
|
|
272
|
+
# @param message [String] content to inject
|
|
273
|
+
# @param label [String, nil] optional label for the injected message
|
|
274
|
+
def chat_inject(session_key:, message:, label: nil)
|
|
275
|
+
ensure_connected!
|
|
276
|
+
touch_activity!
|
|
277
|
+
|
|
278
|
+
params = { sessionKey: session_key, message: message }
|
|
279
|
+
params[:label] = label if label
|
|
280
|
+
send_rpc("chat.inject", params)
|
|
281
|
+
end
|
|
282
|
+
|
|
268
283
|
# Fetch chat history for a session
|
|
269
284
|
def chat_history(session_key:, limit: nil)
|
|
270
285
|
ensure_connected!
|