swarm_sdk 2.7.1 → 2.7.2
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/lib/swarm_sdk/agent/chat.rb +8 -0
- data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +106 -3
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +7 -0
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +5 -2
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +20 -2
- data/lib/swarm_sdk/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: 204f0173ac046d5eae63cf3ddca318e3ab0e76a88661e7f4a106d5079a68ee63
|
|
4
|
+
data.tar.gz: 515a43fac5824b6fab886901449ef808288e8e5e2beaacd02502c0e337a3d98f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 94b2a3d8ee2d4cd61ec45a075b5e9cc370d992e47ae2da7e478abe7aa7f81ebaf2c0cd3571072ed02fdfac1eefc07c8eb5bcfd65aefa46c0cc60298b7e2b30ae
|
|
7
|
+
data.tar.gz: 7e527394023723bfabd8fa18c1696b0dc10b14499fe4c2ba3db46e3c8518158d1d5de8553c41d2139eba3c800f17a739c86503a7366c470ccb9e616446695510
|
data/lib/swarm_sdk/agent/chat.rb
CHANGED
|
@@ -207,6 +207,13 @@ module SwarmSDK
|
|
|
207
207
|
# --- SwarmSDK Abstraction API ---
|
|
208
208
|
# These methods provide SwarmSDK-specific semantics without exposing RubyLLM internals
|
|
209
209
|
|
|
210
|
+
# Check if streaming is enabled for this agent
|
|
211
|
+
#
|
|
212
|
+
# @return [Boolean] true if streaming is enabled
|
|
213
|
+
def streaming_enabled?
|
|
214
|
+
@streaming_enabled
|
|
215
|
+
end
|
|
216
|
+
|
|
210
217
|
# Model information
|
|
211
218
|
def model_id
|
|
212
219
|
@llm_chat.model.id
|
|
@@ -681,6 +688,7 @@ module SwarmSDK
|
|
|
681
688
|
if @streaming_enabled
|
|
682
689
|
# Reset chunk type tracking for new streaming request
|
|
683
690
|
@last_chunk_type = nil
|
|
691
|
+
|
|
684
692
|
@llm_chat.complete(**options) do |chunk|
|
|
685
693
|
emit_content_chunk(chunk)
|
|
686
694
|
end
|
|
@@ -82,6 +82,88 @@ module SwarmSDK
|
|
|
82
82
|
|
|
83
83
|
private
|
|
84
84
|
|
|
85
|
+
# Format citations for appending to response content
|
|
86
|
+
#
|
|
87
|
+
# Creates a markdown-formatted citations section with numbered links.
|
|
88
|
+
#
|
|
89
|
+
# @param citations [Array<String>] Array of citation URLs
|
|
90
|
+
# @return [String] Formatted citations section
|
|
91
|
+
def format_citations(citations)
|
|
92
|
+
return "" if citations.nil? || citations.empty?
|
|
93
|
+
|
|
94
|
+
formatted = "\n\n# Citations\n"
|
|
95
|
+
citations.each_with_index do |citation, index|
|
|
96
|
+
formatted += "- [#{index + 1}] #{citation}\n"
|
|
97
|
+
end
|
|
98
|
+
formatted
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Emit citations as a content_chunk event
|
|
102
|
+
#
|
|
103
|
+
# @param formatted_citations [String] Formatted citations text
|
|
104
|
+
# @param model_id [String] Model identifier
|
|
105
|
+
# @return [void]
|
|
106
|
+
def emit_citations_chunk(formatted_citations, model_id)
|
|
107
|
+
LogStream.emit(
|
|
108
|
+
type: "content_chunk",
|
|
109
|
+
agent: @agent_context.name,
|
|
110
|
+
chunk_type: "citations",
|
|
111
|
+
content: formatted_citations,
|
|
112
|
+
tool_calls: nil,
|
|
113
|
+
model: model_id,
|
|
114
|
+
)
|
|
115
|
+
rescue StandardError => e
|
|
116
|
+
RubyLLM.logger.debug("SwarmSDK: Failed to emit citations chunk: #{e.message}")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Extract citations and search results from an assistant message
|
|
120
|
+
#
|
|
121
|
+
# These fields are provided by some LLM providers (e.g., Perplexity's sonar models)
|
|
122
|
+
# when the model performs web search or cites sources.
|
|
123
|
+
#
|
|
124
|
+
# @param message [RubyLLM::Message] Assistant message with potential citations
|
|
125
|
+
# @return [Hash] Citations and search results (empty if not present)
|
|
126
|
+
def extract_citations_and_search(message)
|
|
127
|
+
return {} unless message.raw&.body
|
|
128
|
+
|
|
129
|
+
body = message.raw.body
|
|
130
|
+
|
|
131
|
+
# For streaming responses, body might be empty - check Fiber-local
|
|
132
|
+
# (set by LLMInstrumentationMiddleware with accumulated SSE chunks)
|
|
133
|
+
if body.is_a?(String) && body.empty?
|
|
134
|
+
fiber_body = Fiber[:last_sse_body]
|
|
135
|
+
body = fiber_body if fiber_body
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
return {} unless body
|
|
139
|
+
|
|
140
|
+
# Handle SSE streaming responses (body is a string starting with "data:")
|
|
141
|
+
if body.is_a?(String) && body.start_with?("data:")
|
|
142
|
+
# Parse the LAST SSE event which contains citations
|
|
143
|
+
last_data_line = body.split("\n").reverse.find { |l| l.start_with?("data:") && !l.include?("[DONE]") && !l.include?("message_stop") }
|
|
144
|
+
if last_data_line
|
|
145
|
+
body = JSON.parse(last_data_line.sub(/^data:\s*/, ""))
|
|
146
|
+
end
|
|
147
|
+
elsif body.is_a?(String)
|
|
148
|
+
# Regular JSON string response
|
|
149
|
+
body = JSON.parse(body)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Handle Faraday::Response objects (has .body method)
|
|
153
|
+
body = body.body if body.respond_to?(:body) && !body.is_a?(Hash)
|
|
154
|
+
|
|
155
|
+
return {} unless body.is_a?(Hash)
|
|
156
|
+
|
|
157
|
+
result = {}
|
|
158
|
+
result[:citations] = body["citations"] if body["citations"]
|
|
159
|
+
result[:search_results] = body["search_results"] if body["search_results"]
|
|
160
|
+
result
|
|
161
|
+
rescue StandardError => e
|
|
162
|
+
# Includes JSON::ParserError and other parsing errors
|
|
163
|
+
RubyLLM.logger.debug("SwarmSDK: Failed to extract citations: #{e.message}")
|
|
164
|
+
{}
|
|
165
|
+
end
|
|
166
|
+
|
|
85
167
|
# Extract usage information from an assistant message
|
|
86
168
|
#
|
|
87
169
|
# @param message [RubyLLM::Message] Assistant message with usage data
|
|
@@ -202,6 +284,7 @@ module SwarmSDK
|
|
|
202
284
|
return unless @chat.hook_executor
|
|
203
285
|
|
|
204
286
|
usage_info = extract_usage_info(message)
|
|
287
|
+
citations_data = extract_citations_and_search(message)
|
|
205
288
|
|
|
206
289
|
context = Hooks::Context.new(
|
|
207
290
|
event: :agent_step,
|
|
@@ -213,9 +296,11 @@ module SwarmSDK
|
|
|
213
296
|
tool_calls: format_tool_calls(message.tool_calls),
|
|
214
297
|
finish_reason: "tool_calls",
|
|
215
298
|
usage: usage_info,
|
|
299
|
+
citations: citations_data[:citations],
|
|
300
|
+
search_results: citations_data[:search_results],
|
|
216
301
|
tool_executions: tool_executions.empty? ? nil : tool_executions,
|
|
217
302
|
timestamp: Time.now.utc.iso8601,
|
|
218
|
-
},
|
|
303
|
+
}.compact,
|
|
219
304
|
)
|
|
220
305
|
|
|
221
306
|
agent_hooks = @chat.hook_agent_hooks[:agent_step] || []
|
|
@@ -238,6 +323,22 @@ module SwarmSDK
|
|
|
238
323
|
return unless @chat.hook_executor
|
|
239
324
|
|
|
240
325
|
usage_info = extract_usage_info(message)
|
|
326
|
+
citations_data = extract_citations_and_search(message)
|
|
327
|
+
|
|
328
|
+
# Format content with citations appended
|
|
329
|
+
content_with_citations = message.content
|
|
330
|
+
if citations_data[:citations] && !citations_data[:citations].empty?
|
|
331
|
+
formatted_citations = format_citations(citations_data[:citations])
|
|
332
|
+
content_with_citations = message.content + formatted_citations
|
|
333
|
+
|
|
334
|
+
# Also modify the original message for Result.content
|
|
335
|
+
message.content = content_with_citations
|
|
336
|
+
|
|
337
|
+
# Emit citations chunk if streaming is enabled
|
|
338
|
+
if @chat.streaming_enabled?
|
|
339
|
+
emit_citations_chunk(formatted_citations, message.model_id)
|
|
340
|
+
end
|
|
341
|
+
end
|
|
241
342
|
|
|
242
343
|
# Use override if set (e.g., "finish_agent"), otherwise default to "stop"
|
|
243
344
|
finish_reason = @finish_reason_override || "stop"
|
|
@@ -249,13 +350,15 @@ module SwarmSDK
|
|
|
249
350
|
swarm: @chat.hook_swarm,
|
|
250
351
|
metadata: {
|
|
251
352
|
model: message.model_id,
|
|
252
|
-
content:
|
|
353
|
+
content: content_with_citations, # Content with citations appended
|
|
253
354
|
tool_calls: nil, # Final response has no tool calls
|
|
254
355
|
finish_reason: finish_reason,
|
|
255
356
|
usage: usage_info,
|
|
357
|
+
citations: citations_data[:citations],
|
|
358
|
+
search_results: citations_data[:search_results],
|
|
256
359
|
tool_executions: tool_executions.empty? ? nil : tool_executions,
|
|
257
360
|
timestamp: Time.now.utc.iso8601,
|
|
258
|
-
},
|
|
361
|
+
}.compact,
|
|
259
362
|
)
|
|
260
363
|
|
|
261
364
|
agent_hooks = @chat.hook_agent_hooks[:agent_stop] || []
|
|
@@ -13,10 +13,15 @@ module SwarmSDK
|
|
|
13
13
|
#
|
|
14
14
|
# @return [void]
|
|
15
15
|
def inject_llm_instrumentation
|
|
16
|
+
puts "=== inject_llm_instrumentation called ===" if ENV["DEBUG_CITATIONS"]
|
|
16
17
|
return unless @provider
|
|
17
18
|
|
|
19
|
+
puts "=== Has provider ===" if ENV["DEBUG_CITATIONS"]
|
|
20
|
+
|
|
18
21
|
faraday_conn = @provider.connection&.connection
|
|
19
22
|
return unless faraday_conn
|
|
23
|
+
|
|
24
|
+
puts "=== Has faraday connection ===" if ENV["DEBUG_CITATIONS"]
|
|
20
25
|
return if @llm_instrumentation_injected
|
|
21
26
|
|
|
22
27
|
provider_name = @provider.class.name.split("::").last.downcase
|
|
@@ -31,8 +36,10 @@ module SwarmSDK
|
|
|
31
36
|
|
|
32
37
|
@llm_instrumentation_injected = true
|
|
33
38
|
|
|
39
|
+
puts "=== MIDDLEWARE INJECTED ===" if ENV["DEBUG_CITATIONS"]
|
|
34
40
|
RubyLLM.logger.debug("SwarmSDK: Injected LLM instrumentation middleware for agent #{@agent_name}")
|
|
35
41
|
rescue StandardError => e
|
|
42
|
+
puts "=== MIDDLEWARE INJECTION ERROR: #{e.message} ===" if ENV["DEBUG_CITATIONS"]
|
|
36
43
|
LogStream.emit_error(e, source: "instrumentation", context: "inject_middleware", agent: @agent_name)
|
|
37
44
|
RubyLLM.logger.debug("SwarmSDK: Failed to inject LLM instrumentation: #{e.message}")
|
|
38
45
|
end
|
|
@@ -39,8 +39,6 @@ module SwarmSDK
|
|
|
39
39
|
emit_request_event(env, start_time)
|
|
40
40
|
|
|
41
41
|
# Wrap existing on_data to capture raw SSE chunks for streaming
|
|
42
|
-
# This allows us to capture the full streaming response for instrumentation
|
|
43
|
-
# Check if env.request exists and has on_data (only set for streaming requests)
|
|
44
42
|
if env.request&.on_data
|
|
45
43
|
original_on_data = env.request.on_data
|
|
46
44
|
env.request.on_data = proc do |chunk, bytes, response_env|
|
|
@@ -64,6 +62,11 @@ module SwarmSDK
|
|
|
64
62
|
response_env.body
|
|
65
63
|
end
|
|
66
64
|
|
|
65
|
+
# Store SSE body in Fiber-local for citation extraction
|
|
66
|
+
# This allows append_citations_to_content to access the full SSE body
|
|
67
|
+
# even though response.body is empty for streaming responses
|
|
68
|
+
Fiber[:last_sse_body] = raw_body if accumulated_raw_chunks.any?
|
|
69
|
+
|
|
67
70
|
# Emit response event
|
|
68
71
|
emit_response_event(response_env, start_time, end_time, duration, raw_body)
|
|
69
72
|
end
|
|
@@ -238,7 +238,14 @@ module SwarmSDK
|
|
|
238
238
|
return unless LogStream.emitter
|
|
239
239
|
|
|
240
240
|
metadata_without_duplicates = context.metadata.except(
|
|
241
|
-
:model,
|
|
241
|
+
:model,
|
|
242
|
+
:content,
|
|
243
|
+
:tool_calls,
|
|
244
|
+
:finish_reason,
|
|
245
|
+
:usage,
|
|
246
|
+
:tool_executions,
|
|
247
|
+
:citations,
|
|
248
|
+
:search_results,
|
|
242
249
|
)
|
|
243
250
|
|
|
244
251
|
LogStream.emit(
|
|
@@ -251,6 +258,8 @@ module SwarmSDK
|
|
|
251
258
|
tool_calls: context.metadata[:tool_calls],
|
|
252
259
|
finish_reason: context.metadata[:finish_reason],
|
|
253
260
|
usage: context.metadata[:usage],
|
|
261
|
+
citations: context.metadata[:citations],
|
|
262
|
+
search_results: context.metadata[:search_results],
|
|
254
263
|
tool_executions: context.metadata[:tool_executions],
|
|
255
264
|
metadata: metadata_without_duplicates,
|
|
256
265
|
)
|
|
@@ -261,7 +270,14 @@ module SwarmSDK
|
|
|
261
270
|
return unless LogStream.emitter
|
|
262
271
|
|
|
263
272
|
metadata_without_duplicates = context.metadata.except(
|
|
264
|
-
:model,
|
|
273
|
+
:model,
|
|
274
|
+
:content,
|
|
275
|
+
:tool_calls,
|
|
276
|
+
:finish_reason,
|
|
277
|
+
:usage,
|
|
278
|
+
:tool_executions,
|
|
279
|
+
:citations,
|
|
280
|
+
:search_results,
|
|
265
281
|
)
|
|
266
282
|
|
|
267
283
|
LogStream.emit(
|
|
@@ -274,6 +290,8 @@ module SwarmSDK
|
|
|
274
290
|
tool_calls: context.metadata[:tool_calls],
|
|
275
291
|
finish_reason: context.metadata[:finish_reason],
|
|
276
292
|
usage: context.metadata[:usage],
|
|
293
|
+
citations: context.metadata[:citations],
|
|
294
|
+
search_results: context.metadata[:search_results],
|
|
277
295
|
tool_executions: context.metadata[:tool_executions],
|
|
278
296
|
metadata: metadata_without_duplicates,
|
|
279
297
|
)
|
data/lib/swarm_sdk/version.rb
CHANGED