openclacky 0.8.5 → 0.8.6
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/CHANGELOG.md +27 -0
- data/docs/channel-architecture.md +235 -0
- data/lib/clacky/agent/memory_updater.rb +1 -0
- data/lib/clacky/agent/session_serializer.rb +41 -3
- data/lib/clacky/agent/skill_manager.rb +1 -1
- data/lib/clacky/brand_config.rb +352 -43
- data/lib/clacky/cli.rb +5 -4
- data/lib/clacky/client.rb +2 -2
- data/lib/clacky/default_skills/channel-setup/SKILL.md +277 -0
- data/lib/clacky/default_skills/cron-task-creator/SKILL.md +250 -0
- data/lib/clacky/default_skills/cron-task-creator/evals/evals.json +38 -0
- data/lib/clacky/default_skills/cron-task-creator/scripts/list_tasks.rb +121 -0
- data/lib/clacky/default_skills/cron-task-creator/scripts/manage_schedule.rb +149 -0
- data/lib/clacky/default_skills/cron-task-creator/scripts/manage_task.rb +81 -0
- data/lib/clacky/default_skills/cron-task-creator/scripts/task_history.rb +137 -0
- data/lib/clacky/default_skills/skill-add/SKILL.md +21 -260
- data/lib/clacky/default_skills/skill-add/scripts/install_from_github.rb +143 -99
- data/lib/clacky/default_skills/skill-creator/SKILL.md +547 -0
- data/lib/clacky/default_skills/skill-creator/agents/analyzer.md +274 -0
- data/lib/clacky/default_skills/skill-creator/agents/comparator.md +202 -0
- data/lib/clacky/default_skills/skill-creator/agents/grader.md +223 -0
- data/lib/clacky/default_skills/skill-creator/eval-viewer/generate_review.py +471 -0
- data/lib/clacky/default_skills/skill-creator/eval-viewer/viewer.html +1325 -0
- data/lib/clacky/default_skills/skill-creator/references/schemas.md +430 -0
- data/lib/clacky/default_skills/skill-creator/scripts/__init__.py +0 -0
- data/lib/clacky/default_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- data/lib/clacky/default_skills/skill-creator/scripts/generate_report.py +326 -0
- data/lib/clacky/default_skills/skill-creator/scripts/improve_description.py +310 -0
- data/lib/clacky/default_skills/skill-creator/scripts/quick_validate.py +103 -0
- data/lib/clacky/default_skills/skill-creator/scripts/run_eval.py +317 -0
- data/lib/clacky/default_skills/skill-creator/scripts/run_loop.py +331 -0
- data/lib/clacky/default_skills/skill-creator/scripts/utils.py +47 -0
- data/lib/clacky/server/channel/adapters/base.rb +82 -0
- data/lib/clacky/server/channel/adapters/feishu/adapter.rb +172 -0
- data/lib/clacky/server/channel/adapters/feishu/bot.rb +191 -0
- data/lib/clacky/server/channel/adapters/feishu/message_parser.rb +106 -0
- data/lib/clacky/server/channel/adapters/feishu/ws_client.rb +385 -0
- data/lib/clacky/server/channel/adapters/wecom/adapter.rb +106 -0
- data/lib/clacky/server/channel/adapters/wecom/ws_client.rb +188 -0
- data/lib/clacky/server/channel/channel_config.rb +146 -0
- data/lib/clacky/server/channel/channel_manager.rb +230 -0
- data/lib/clacky/server/channel/channel_ui_controller.rb +179 -0
- data/lib/clacky/server/channel.rb +29 -0
- data/lib/clacky/server/http_server.rb +323 -9
- data/lib/clacky/server/web_ui_controller.rb +73 -1
- data/lib/clacky/skill_loader.rb +1 -0
- data/lib/clacky/tools/browser.rb +281 -43
- data/lib/clacky/utils/logger.rb +20 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +452 -17
- data/lib/clacky/web/app.js +53 -18
- data/lib/clacky/web/channels.js +196 -0
- data/lib/clacky/web/index.html +29 -6
- data/lib/clacky/web/sessions.js +10 -1
- data/lib/clacky/web/settings.js +2 -2
- data/lib/clacky/web/skills.js +307 -92
- data/lib/clacky/web/tasks.js +2 -2
- metadata +36 -2
- data/lib/clacky/default_skills/create-task/SKILL.md +0 -102
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0190a435c18d666a4d2cc7c65b301bd24f18cc6a0159f5d7a4ae25ee552883d7'
|
|
4
|
+
data.tar.gz: 62eecd22f6cc112aa00674c683002f14ada01d637b95551a5ca7ff29d408ad75
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8b0dcb369eeb481fc32dd8e02d4cc22ed6b21f3fb5115d9145623444bb88be1cf95cf1c3f8b62988bd54c28888512ee90ecf4df74f98250a04f2f0fcbd9d77d8
|
|
7
|
+
data.tar.gz: a6d72920b58547540dd6b389cb8c5dadafc4cf9face794217a7d4c2245bb4f2b46fce4e83616074d1054b9f7542ee0601cad1f9e43fc9358563f6d0dc8b97124
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.6] - 2026-03-12
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Channel system with Feishu & WeCom support**: integrated IM platform adapters — agents can now receive and reply to messages via Feishu (WebSocket) and WeCom channels
|
|
14
|
+
- **Skill encryption (brand skills)**: brand skills can be distributed as encrypted `.enc` files, decrypted on-the-fly using license keys; includes a full key management and manifest system
|
|
15
|
+
- **Cron task creator & skill creator default skills**: two new built-in skills for creating scheduled tasks and new skills directly from chat
|
|
16
|
+
- **Image messages in session history restore**: session restore now correctly replays image-containing messages, including thumbnail display in the UI
|
|
17
|
+
- **Skill auto-upload to cloud**: skills can be uploaded to the cloud store from within the UI
|
|
18
|
+
|
|
19
|
+
### Improved
|
|
20
|
+
- **WeCom setup flow**: improved step-by-step WeCom channel configuration UX (#11)
|
|
21
|
+
- **Skill autocomplete UI**: enhanced slash-command autocomplete interaction — better keyboard navigation, input behavior, and visual feedback (#6)
|
|
22
|
+
- **Chrome setup UX**: simplified Chrome installation flow with improved error messages and progress indicators (#8)
|
|
23
|
+
- **WebUI colors and layout**: polished light/dark mode colors, sidebar alignment, and badge styles for a more consistent look
|
|
24
|
+
- **Test suite speed**: `CLACKY_TEST` guard prevents brand skill network calls during tests — suite now runs ~60× faster per example
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- **Duplicate user bubble on skill install**: prevented an extra chat bubble appearing when installing a skill from the store
|
|
28
|
+
- **Image thumbnails in session replay**: restored missing image thumbnails when replaying historical sessions
|
|
29
|
+
- **WebUI permission mode**: Web UI sessions now correctly use `confirm_all` permission mode
|
|
30
|
+
- **Feishu WS log noise**: removed emoji characters from WebSocket connection log messages
|
|
31
|
+
|
|
32
|
+
### More
|
|
33
|
+
- Subagent memory update disabled to reduce noise
|
|
34
|
+
- Ping request `max_tokens` bumped from 10 to 16
|
|
35
|
+
- WebUI updated to use new cron-task-creator and skill-creator skills
|
|
36
|
+
|
|
10
37
|
## [0.8.5] - 2026-03-11
|
|
11
38
|
|
|
12
39
|
### Fixed
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Channel Architecture
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Channel is a feature that bridges Clacky's Server Sessions to IM platforms
|
|
6
|
+
(Feishu, WeCom, DingTalk, etc.). It reuses the existing Agent + SessionRegistry
|
|
7
|
+
infrastructure — the Agent knows nothing about IM; the Channel layer is purely
|
|
8
|
+
a transport adapter.
|
|
9
|
+
|
|
10
|
+
## Design Principles
|
|
11
|
+
|
|
12
|
+
- **Zero Agent intrusion** — Agent only speaks `UIInterface`; swap the controller, get IM output
|
|
13
|
+
- **Reuse SessionRegistry** — IM chats resolve to the same `SessionRegistry` sessions as Web UI
|
|
14
|
+
- **WebSocket long connection** — No public domain required; adapters hold a persistent WSS connection to the IM platform
|
|
15
|
+
- **One platform = 2 threads** — read loop thread + ping/heartbeat thread (constant, small footprint)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Layer Diagram
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
IM Platforms (Feishu / WeCom / DingTalk)
|
|
23
|
+
│ WebSocket long connection (wss://)
|
|
24
|
+
▼
|
|
25
|
+
┌─────────────────────────────────────┐
|
|
26
|
+
│ Channel Adapter Layer │
|
|
27
|
+
│ Feishu::Adapter │
|
|
28
|
+
│ ├── WSClient (read loop + ping) │
|
|
29
|
+
│ ├── Bot (send API) │
|
|
30
|
+
│ └── MessageParser │
|
|
31
|
+
│ Wecom::Adapter │
|
|
32
|
+
│ └── WSClient (read loop + ping) │
|
|
33
|
+
│ (future) Dingtalk::Adapter │
|
|
34
|
+
└──────────────┬──────────────────────┘
|
|
35
|
+
│ standardized event Hash
|
|
36
|
+
▼
|
|
37
|
+
┌─────────────────────────────────────┐
|
|
38
|
+
│ ChannelManager │
|
|
39
|
+
│ • Owns adapter threads │
|
|
40
|
+
│ • Routes inbound event → │
|
|
41
|
+
│ ChannelBinding → session_id │
|
|
42
|
+
│ • Calls agent.run in Thread.new │
|
|
43
|
+
└──────────────┬──────────────────────┘
|
|
44
|
+
│
|
|
45
|
+
┌───────┴────────┐
|
|
46
|
+
▼ ▼
|
|
47
|
+
SessionRegistry ChannelUIController
|
|
48
|
+
(existing) (implements UIInterface)
|
|
49
|
+
│ │
|
|
50
|
+
▼ ▼
|
|
51
|
+
Agent IM Platform reply
|
|
52
|
+
(unchanged) via adapter.send_text
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## File Structure
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
lib/clacky/channel/
|
|
61
|
+
├── adapters/
|
|
62
|
+
│ ├── base.rb # Adapter abstract base + registry
|
|
63
|
+
│ ├── feishu/
|
|
64
|
+
│ │ ├── adapter.rb # Feishu::Adapter < Base
|
|
65
|
+
│ │ ├── bot.rb # HTTP send API (token cache, Markdown/card)
|
|
66
|
+
│ │ ├── message_parser.rb # Raw WS event → standardized Hash
|
|
67
|
+
│ │ └── ws_client.rb # Feishu protobuf WS long connection
|
|
68
|
+
│ └── wecom/
|
|
69
|
+
│ ├── adapter.rb # Wecom::Adapter < Base
|
|
70
|
+
│ └── ws_client.rb # WeCom JSON WS long connection
|
|
71
|
+
├── channel_message.rb # Struct: standardized inbound message
|
|
72
|
+
├── channel_binding.rb # (platform, user_id) → session_id mapping
|
|
73
|
+
├── channel_ui_controller.rb # UIInterface impl — pushes events to IM
|
|
74
|
+
└── channel_manager.rb # Lifecycle: start/stop adapters, route messages
|
|
75
|
+
lib/clacky/channel.rb # Top-level require entry point
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Standardized Inbound Event
|
|
81
|
+
|
|
82
|
+
All adapters yield the same Hash shape to `ChannelManager`:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
{
|
|
86
|
+
platform: :feishu, # Symbol
|
|
87
|
+
chat_id: "oc_xxx", # String — IM chat/group identifier
|
|
88
|
+
user_id: "ou_xxx", # String — IM user identifier
|
|
89
|
+
text: "deploy now", # String — cleaned user text
|
|
90
|
+
message_id: "om_xxx", # String — for threading / update
|
|
91
|
+
timestamp: Time, # Time object
|
|
92
|
+
chat_type: :direct | :group, # Symbol
|
|
93
|
+
raw: { ... } # Original platform payload
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Adapter Interface (Base)
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
class Adapters::Base
|
|
103
|
+
def self.platform_id → Symbol
|
|
104
|
+
def self.platform_config(raw_config) → Hash # symbol-keyed
|
|
105
|
+
def self.env_keys → Array<String> # for config serialization
|
|
106
|
+
|
|
107
|
+
def start(&on_message) # blocks; yields event Hash per inbound message
|
|
108
|
+
def stop # graceful shutdown
|
|
109
|
+
def send_text(chat_id, text, reply_to: nil) → Hash
|
|
110
|
+
def update_message(chat_id, message_id, text) → Boolean
|
|
111
|
+
def supports_message_updates? → Boolean
|
|
112
|
+
def validate_config(config) → Array<String> # error messages
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## ChannelManager
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
class ChannelManager
|
|
122
|
+
def initialize(session_registry:, session_builder:, channel_config:, agent_config:)
|
|
123
|
+
|
|
124
|
+
def start # Thread.new per enabled platform adapter
|
|
125
|
+
def stop # kills all adapter threads gracefully
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def route_message(adapter, event)
|
|
130
|
+
session_id = @binding.resolve_or_create(event, session_builder: @session_builder)
|
|
131
|
+
ui = ChannelUIController.new(event, adapter)
|
|
132
|
+
Thread.new { run_agent(session_id, event[:text], ui) }
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## ChannelBinding
|
|
140
|
+
|
|
141
|
+
Maps `(platform, user_id)` → `session_id`. Persisted to `~/.clacky/channel_bindings.yml`.
|
|
142
|
+
|
|
143
|
+
Binding modes (configurable per platform):
|
|
144
|
+
|
|
145
|
+
| Mode | Key | Description |
|
|
146
|
+
|------|-----|-------------|
|
|
147
|
+
| `user` | `(platform, user_id)` | Each IM user gets their own session (default) |
|
|
148
|
+
| `chat` | `(platform, chat_id)` | Whole group shares one session |
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## ChannelUIController
|
|
153
|
+
|
|
154
|
+
Implements `UIInterface`. Key behaviours:
|
|
155
|
+
|
|
156
|
+
- `show_assistant_message` → `adapter.send_text(chat_id, content)`
|
|
157
|
+
- `show_tool_call` → buffers as `⚙️ \`tool summary\`` (flushed on next message)
|
|
158
|
+
- `show_progress` → `adapter.update_message(...)` if `supports_message_updates?`
|
|
159
|
+
- `show_complete` → sends `✅ Complete • N iterations • $cost`
|
|
160
|
+
- `request_confirmation` → **not supported in IM** (returns auto-approved / raises)
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Thread Model
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
Main thread (WEBrick server.start — blocks)
|
|
168
|
+
├── WEBrick request threads (existing)
|
|
169
|
+
├── Agent task threads (existing, per task)
|
|
170
|
+
├── Scheduler thread (existing, clacky-scheduler)
|
|
171
|
+
└── ChannelManager
|
|
172
|
+
├── feishu-adapter thread (WSClient read loop, constant)
|
|
173
|
+
│ └── feishu-ping thread (heartbeat, 90s)
|
|
174
|
+
└── wecom-adapter thread (WSClient read loop, constant)
|
|
175
|
+
└── wecom-ping thread (heartbeat, 30s)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Per enabled platform: **2 constant threads**. Agent task threads are spawned
|
|
179
|
+
on demand (same as Web UI path) and exit when done.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Configuration
|
|
184
|
+
|
|
185
|
+
Channel credentials live in `~/.clacky/channels.yml` (managed by `ChannelConfig`
|
|
186
|
+
which already exists in main branch):
|
|
187
|
+
|
|
188
|
+
```yaml
|
|
189
|
+
channels:
|
|
190
|
+
feishu:
|
|
191
|
+
enabled: true
|
|
192
|
+
app_id: cli_xxx
|
|
193
|
+
app_secret: xxx
|
|
194
|
+
allowed_users:
|
|
195
|
+
- ou_xxx
|
|
196
|
+
wecom:
|
|
197
|
+
enabled: false
|
|
198
|
+
bot_id: xxx
|
|
199
|
+
secret: xxx
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`ChannelManager` reads this via `ChannelConfig#platform_config(platform)`.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Integration with HttpServer
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
# HttpServer#initialize
|
|
210
|
+
@channel_manager = ChannelManager.new(
|
|
211
|
+
session_registry: @registry,
|
|
212
|
+
session_builder: method(:build_session),
|
|
213
|
+
channel_config: Clacky::ChannelConfig.load,
|
|
214
|
+
agent_config: @agent_config
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# HttpServer#start (after scheduler.start)
|
|
218
|
+
@channel_manager.start
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
`ChannelManager#start` is non-blocking (spawns threads internally),
|
|
222
|
+
mirroring `Scheduler#start` behaviour.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Future: DingTalk
|
|
227
|
+
|
|
228
|
+
DingTalk also supports a WebSocket Stream mode. Adding it means:
|
|
229
|
+
|
|
230
|
+
1. `lib/clacky/channel/adapters/dingtalk/adapter.rb` inheriting `Base`
|
|
231
|
+
2. `lib/clacky/channel/adapters/dingtalk/ws_client.rb`
|
|
232
|
+
3. Register: `Adapters.register(:dingtalk, Adapter)`
|
|
233
|
+
4. Add credentials to `ChannelConfig`
|
|
234
|
+
|
|
235
|
+
No changes needed to `ChannelManager`, `ChannelUIController`, or `ChannelBinding`.
|
|
@@ -26,6 +26,7 @@ module Clacky
|
|
|
26
26
|
# @return [Boolean]
|
|
27
27
|
def should_update_memory?
|
|
28
28
|
return false unless memory_update_enabled?
|
|
29
|
+
return false if @is_subagent # Subagents never update memory
|
|
29
30
|
|
|
30
31
|
task_iterations = @iterations - (@task_start_iterations || 0)
|
|
31
32
|
task_iterations >= MEMORY_UPDATE_MIN_ITERATIONS
|
|
@@ -153,8 +153,20 @@ module Clacky
|
|
|
153
153
|
@messages.each do |msg|
|
|
154
154
|
role = msg[:role].to_s
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
# A real user message can have either a String content or an Array content
|
|
157
|
+
# (Array = multipart: text + image blocks). Exclude system-injected messages
|
|
158
|
+
# and synthetic [SYSTEM] text messages.
|
|
159
|
+
is_real_user_msg = role == "user" && !msg[:system_injected] &&
|
|
160
|
+
if msg[:content].is_a?(String)
|
|
161
|
+
!msg[:content].start_with?("[SYSTEM]")
|
|
162
|
+
elsif msg[:content].is_a?(Array)
|
|
163
|
+
# Must contain at least one text or image block (not a tool_result array)
|
|
164
|
+
msg[:content].any? { |b| b.is_a?(Hash) && %w[text image].include?(b[:type].to_s) }
|
|
165
|
+
else
|
|
166
|
+
false
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
if is_real_user_msg
|
|
158
170
|
# Start a new round at each real user message
|
|
159
171
|
current_round = { user_msg: msg, events: [] }
|
|
160
172
|
rounds << current_round
|
|
@@ -175,8 +187,10 @@ module Clacky
|
|
|
175
187
|
page.each do |round|
|
|
176
188
|
msg = round[:user_msg]
|
|
177
189
|
display_text = extract_text_from_content(msg[:content])
|
|
190
|
+
# Extract image data URLs from multipart content (for history replay rendering)
|
|
191
|
+
images = extract_images_from_content(msg[:content])
|
|
178
192
|
# Emit user message with its timestamp for dedup on the frontend
|
|
179
|
-
ui.show_user_message(display_text, created_at: msg[:created_at])
|
|
193
|
+
ui.show_user_message(display_text, created_at: msg[:created_at], images: images)
|
|
180
194
|
|
|
181
195
|
round[:events].each do |ev|
|
|
182
196
|
# Skip system-injected messages (e.g. synthetic skill content, memory prompts)
|
|
@@ -241,6 +255,30 @@ module Clacky
|
|
|
241
255
|
Clacky::Logger.warn("refresh_system_prompt failed during session restore: #{e.message}")
|
|
242
256
|
end
|
|
243
257
|
|
|
258
|
+
# Extract base64 data URLs from multipart content (image blocks).
|
|
259
|
+
# Returns an empty array when there are no images or content is plain text.
|
|
260
|
+
# @param content [String, Array, Object] Message content
|
|
261
|
+
# @return [Array<String>] Array of data URLs (e.g. "data:image/png;base64,...")
|
|
262
|
+
def extract_images_from_content(content)
|
|
263
|
+
return [] unless content.is_a?(Array)
|
|
264
|
+
|
|
265
|
+
content.filter_map do |block|
|
|
266
|
+
next unless block.is_a?(Hash)
|
|
267
|
+
|
|
268
|
+
case block[:type].to_s
|
|
269
|
+
when "image_url"
|
|
270
|
+
# OpenAI format: { type: "image_url", image_url: { url: "data:image/png;base64,..." } }
|
|
271
|
+
block.dig(:image_url, :url)
|
|
272
|
+
when "image"
|
|
273
|
+
# Anthropic format: { type: "image", source: { type: "base64", media_type: "image/png", data: "..." } }
|
|
274
|
+
source = block[:source]
|
|
275
|
+
next unless source.is_a?(Hash) && source[:type].to_s == "base64"
|
|
276
|
+
|
|
277
|
+
"data:#{source[:media_type]};base64,#{source[:data]}"
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
244
282
|
# Extract text from message content (handles string and array formats)
|
|
245
283
|
# @param content [String, Array, Object] Message content
|
|
246
284
|
# @return [String] Extracted text
|