skald 0.1.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.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +620 -0
  4. data/lib/skald.rb +335 -0
  5. metadata +106 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 914d138d114eae0c785f900d311427ff2967b6cddae2fc3a47fa4e8307d1c5a9
4
+ data.tar.gz: c394e54190914540797834c4587e10b1d335d481d01450ee4362ad54322b851d
5
+ SHA512:
6
+ metadata.gz: 7c964cda5f764004bd9b8c2f5d2ce40708d1a164ae684bc94c7a7ba235a2783656cd58571d073ee99049ed0f3c6a2416cb1af6defaa1cd25da9f3661f70d6b65
7
+ data.tar.gz: 369bb564e34ee643b0fb9bd11593f0c583f7526269a8917415d71d58631202f210a9ad4af1ec807b8c68e44aab7b196b5a130c09f53452126ae6848b2ceef0a0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Skald
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,620 @@
1
+ # Skald Ruby SDK
2
+
3
+ Ruby client library for [Skald](https://useskald.com) - an API platform that allows you to easily push context/knowledge via our API (in the form of memos) and get access to chat, document generation, and semantic search out-of-the-box.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'skald'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```bash
16
+ bundle install
17
+ ```
18
+
19
+ Or install it yourself as:
20
+
21
+ ```bash
22
+ gem install skald
23
+ ```
24
+
25
+ ## Requirements
26
+
27
+ - Ruby 3.0.0 or higher
28
+
29
+ ## Quick Start
30
+
31
+ ```ruby
32
+ require 'skald'
33
+
34
+ # Initialize the client
35
+ client = Skald.new('your-api-key')
36
+
37
+ # Create a memo
38
+ client.create_memo(
39
+ title: "Meeting Notes",
40
+ content: "Discussed project timeline..."
41
+ )
42
+
43
+ # Search your memos
44
+ results = client.search(
45
+ query: "project timeline",
46
+ search_method: "chunk_vector_search"
47
+ )
48
+
49
+ # Chat with your memos
50
+ response = client.chat(query: "What were the main discussion points?")
51
+ puts response[:response]
52
+ ```
53
+
54
+ ## Initialization
55
+
56
+ ```ruby
57
+ # Default base URL (https://api.useskald.com)
58
+ client = Skald.new('your-api-key')
59
+
60
+ # Custom base URL (for self-hosted or testing)
61
+ client = Skald.new('your-api-key', 'https://custom.api.com')
62
+ ```
63
+
64
+ ## Memo Management
65
+
66
+ ### Create Memo
67
+
68
+ Create a new memo with content and optional metadata.
69
+
70
+ ```ruby
71
+ result = client.create_memo(
72
+ title: "Project Planning Meeting",
73
+ content: "Discussed Q1 goals, resource allocation, and timeline...",
74
+ metadata: { priority: "high", department: "engineering" },
75
+ tags: ["meeting", "planning"],
76
+ source: "notion",
77
+ reference_id: "notion-page-123",
78
+ expiration_date: "2025-12-31T23:59:59Z"
79
+ )
80
+ # => { ok: true }
81
+ ```
82
+
83
+ **Parameters:**
84
+ - `title` (String, required): Memo title (max 255 characters)
85
+ - `content` (String, required): Memo content
86
+ - `metadata` (Hash, optional): Custom JSON metadata
87
+ - `tags` (Array<String>, optional): Array of tags
88
+ - `source` (String, optional): Source system (e.g., "notion", "slack", max 255 chars)
89
+ - `reference_id` (String, optional): External reference ID (max 255 chars)
90
+ - `expiration_date` (String, optional): ISO 8601 expiration timestamp
91
+
92
+ **Response:**
93
+ ```ruby
94
+ { ok: true }
95
+ ```
96
+
97
+ ### Get Memo
98
+
99
+ Retrieve a memo by UUID or reference ID.
100
+
101
+ ```ruby
102
+ # Get by UUID
103
+ memo = client.get_memo("550e8400-e29b-41d4-a716-446655440000")
104
+
105
+ # Get by reference ID
106
+ memo = client.get_memo("notion-page-123", "reference_id")
107
+ ```
108
+
109
+ **Parameters:**
110
+ - `memo_id` (String, required): Memo UUID or reference ID
111
+ - `id_type` (String, optional): `"memo_uuid"` (default) or `"reference_id"`
112
+
113
+ **Response:**
114
+ ```ruby
115
+ {
116
+ uuid: "550e8400-e29b-41d4-a716-446655440000",
117
+ created_at: "2025-01-15T10:30:00Z",
118
+ updated_at: "2025-01-15T10:30:00Z",
119
+ title: "Project Planning Meeting",
120
+ content: "Discussed Q1 goals...",
121
+ summary: "AI-generated summary of the memo",
122
+ content_length: 150,
123
+ metadata: { priority: "high" },
124
+ client_reference_id: "notion-page-123",
125
+ source: "notion",
126
+ type: "memo",
127
+ expiration_date: nil,
128
+ archived: false,
129
+ pending: false,
130
+ tags: [
131
+ { uuid: "tag-uuid", tag: "meeting" }
132
+ ],
133
+ chunks: [
134
+ { uuid: "chunk-uuid", chunk_content: "...", chunk_index: 0 }
135
+ ]
136
+ }
137
+ ```
138
+
139
+ ### List Memos
140
+
141
+ List all memos with pagination.
142
+
143
+ ```ruby
144
+ # Default pagination (page 1, 20 items)
145
+ response = client.list_memos
146
+
147
+ # Custom pagination
148
+ response = client.list_memos(page: 2, page_size: 50)
149
+ ```
150
+
151
+ **Parameters:**
152
+ - `page` (Integer, optional): Page number (default: 1)
153
+ - `page_size` (Integer, optional): Results per page (default: 20, max: 100)
154
+
155
+ **Response:**
156
+ ```ruby
157
+ {
158
+ count: 150,
159
+ next: "https://api.useskald.com/api/v1/memo?page=2",
160
+ previous: nil,
161
+ results: [
162
+ {
163
+ uuid: "...",
164
+ title: "Memo 1",
165
+ summary: "...",
166
+ # ... other memo fields
167
+ }
168
+ ]
169
+ }
170
+ ```
171
+
172
+ ### Update Memo
173
+
174
+ Update an existing memo. Updating `content` triggers reprocessing.
175
+
176
+ ```ruby
177
+ # Update by UUID
178
+ client.update_memo(
179
+ "550e8400-e29b-41d4-a716-446655440000",
180
+ {
181
+ title: "Updated Title",
182
+ metadata: { priority: "medium", status: "reviewed" }
183
+ }
184
+ )
185
+
186
+ # Update by reference ID
187
+ client.update_memo(
188
+ "notion-page-123",
189
+ { content: "New content..." },
190
+ "reference_id"
191
+ )
192
+ ```
193
+
194
+ **Parameters:**
195
+ - `memo_id` (String, required): Memo UUID or reference ID
196
+ - `update_data` (Hash, required): Fields to update
197
+ - `title` (String, optional): New title
198
+ - `content` (String, optional): New content (triggers reprocessing)
199
+ - `metadata` (Hash, optional): New metadata
200
+ - `client_reference_id` (String, optional): New reference ID
201
+ - `source` (String, optional): New source
202
+ - `expiration_date` (String, optional): New expiration date
203
+ - `id_type` (String, optional): `"memo_uuid"` (default) or `"reference_id"`
204
+
205
+ **Response:**
206
+ ```ruby
207
+ { ok: true }
208
+ ```
209
+
210
+ ### Delete Memo
211
+
212
+ Permanently delete a memo and all associated data.
213
+
214
+ ```ruby
215
+ # Delete by UUID
216
+ client.delete_memo("550e8400-e29b-41d4-a716-446655440000")
217
+
218
+ # Delete by reference ID
219
+ client.delete_memo("notion-page-123", "reference_id")
220
+ ```
221
+
222
+ **Parameters:**
223
+ - `memo_id` (String, required): Memo UUID or reference ID
224
+ - `id_type` (String, optional): `"memo_uuid"` (default) or `"reference_id"`
225
+
226
+ **Response:**
227
+ ```ruby
228
+ nil
229
+ ```
230
+
231
+ ## Search
232
+
233
+ Search your memos using semantic search or title matching.
234
+
235
+ ### Search Methods
236
+
237
+ 1. **`chunk_vector_search`**: Semantic/vector search on memo content chunks
238
+ 2. **`title_contains`**: Case-insensitive substring match on titles
239
+ 3. **`title_startswith`**: Case-insensitive prefix match on titles
240
+
241
+ ### Semantic Search
242
+
243
+ ```ruby
244
+ results = client.search(
245
+ query: "project timeline and milestones",
246
+ search_method: "chunk_vector_search",
247
+ limit: 20
248
+ )
249
+ ```
250
+
251
+ ### Title Search
252
+
253
+ ```ruby
254
+ # Contains
255
+ results = client.search(
256
+ query: "meeting",
257
+ search_method: "title_contains"
258
+ )
259
+
260
+ # Starts with
261
+ results = client.search(
262
+ query: "Project",
263
+ search_method: "title_startswith"
264
+ )
265
+ ```
266
+
267
+ **Parameters:**
268
+ - `query` (String, required): Search query
269
+ - `search_method` (String, required): One of `"chunk_vector_search"`, `"title_contains"`, or `"title_startswith"`
270
+ - `limit` (Integer, optional): Maximum results (1-50, default: 10)
271
+ - `filters` (Array<Hash>, optional): Filters to apply (see [Filters](#filters))
272
+
273
+ **Response:**
274
+ ```ruby
275
+ {
276
+ results: [
277
+ {
278
+ uuid: "550e8400-...",
279
+ title: "Project Planning Meeting",
280
+ summary: "Discussed Q1 goals...",
281
+ content_snippet: "...relevant excerpt from the content...",
282
+ distance: 0.45 # For vector search: 0-2, lower is better. nil for title searches
283
+ }
284
+ ]
285
+ }
286
+ ```
287
+
288
+ ## Chat
289
+
290
+ Ask questions and get answers based on your memos with inline citations.
291
+
292
+ ### Non-Streaming Chat
293
+
294
+ ```ruby
295
+ response = client.chat(
296
+ query: "What are the main project goals for Q1?"
297
+ )
298
+
299
+ puts response[:response]
300
+ # => "The main goals are: 1) Launch MVP [[1]], 2) Hire team [[2]]..."
301
+ ```
302
+
303
+ **Parameters:**
304
+ - `query` (String, required): Question to ask
305
+ - `filters` (Array<Hash>, optional): Filters to apply (see [Filters](#filters))
306
+
307
+ **Response:**
308
+ ```ruby
309
+ {
310
+ ok: true,
311
+ response: "Answer with inline citations [[1]], [[2]], etc.",
312
+ intermediate_steps: [] # For debugging
313
+ }
314
+ ```
315
+
316
+ ### Streaming Chat
317
+
318
+ ```ruby
319
+ print "Answer: "
320
+ client.streamed_chat(
321
+ query: "Summarize the meeting notes from last week"
322
+ ).each do |event|
323
+ if event[:type] == "token"
324
+ print event[:content]
325
+ elsif event[:type] == "done"
326
+ puts "\nDone!"
327
+ end
328
+ end
329
+ ```
330
+
331
+ **Parameters:**
332
+ - Same as non-streaming chat
333
+
334
+ **Yields:**
335
+ ```ruby
336
+ { type: "token", content: "Each" }
337
+ { type: "token", content: " word" }
338
+ { type: "done" }
339
+ ```
340
+
341
+ ## Document Generation
342
+
343
+ Generate documents based on your memos with optional style rules.
344
+
345
+ ### Non-Streaming Generation
346
+
347
+ ```ruby
348
+ response = client.generate_doc(
349
+ prompt: "Create a comprehensive project status report",
350
+ rules: "Use bullet points and maintain a professional tone"
351
+ )
352
+
353
+ puts response[:response]
354
+ # => "# Project Status Report\n\n## Overview\nThe project is on track [[1]]..."
355
+ ```
356
+
357
+ **Parameters:**
358
+ - `prompt` (String, required): What document to generate
359
+ - `rules` (String, optional): Style/format rules
360
+ - `filters` (Array<Hash>, optional): Filters to apply (see [Filters](#filters))
361
+
362
+ **Response:**
363
+ ```ruby
364
+ {
365
+ ok: true,
366
+ response: "Generated document with citations [[1]], [[2]]...",
367
+ intermediate_steps: []
368
+ }
369
+ ```
370
+
371
+ ### Streaming Generation
372
+
373
+ ```ruby
374
+ puts "Generated Document:"
375
+ client.streamed_generate_doc(
376
+ prompt: "Write a technical architecture document",
377
+ rules: "Include diagrams descriptions and code examples"
378
+ ).each do |event|
379
+ if event[:type] == "token"
380
+ print event[:content]
381
+ elsif event[:type] == "done"
382
+ puts "\nDone!"
383
+ end
384
+ end
385
+ ```
386
+
387
+ **Parameters:**
388
+ - Same as non-streaming generation
389
+
390
+ **Yields:**
391
+ ```ruby
392
+ { type: "token", content: "Each" }
393
+ { type: "token", content: " word" }
394
+ { type: "done" }
395
+ ```
396
+
397
+ ## Filters
398
+
399
+ Filters allow you to narrow down which memos are used for search, chat, and document generation. Multiple filters use AND logic (all must match).
400
+
401
+ ### Filter Structure
402
+
403
+ ```ruby
404
+ {
405
+ field: "field_name",
406
+ operator: "eq",
407
+ value: "value",
408
+ filter_type: "native_field"
409
+ }
410
+ ```
411
+
412
+ ### Filter Types
413
+
414
+ 1. **`native_field`**: Built-in memo properties
415
+ - `title`: Memo title
416
+ - `source`: Source system
417
+ - `client_reference_id`: Your reference ID
418
+ - `tags`: Memo tags
419
+
420
+ 2. **`custom_metadata`**: User-defined metadata fields
421
+
422
+ ### Filter Operators
423
+
424
+ | Operator | Description | Value Type |
425
+ |----------|-------------|------------|
426
+ | `eq` | Exact match | String |
427
+ | `neq` | Not equals | String |
428
+ | `contains` | Substring match (case-insensitive) | String |
429
+ | `startswith` | Prefix match (case-insensitive) | String |
430
+ | `endswith` | Suffix match (case-insensitive) | String |
431
+ | `in` | Value is in array | Array<String> |
432
+ | `not_in` | Value not in array | Array<String> |
433
+
434
+ ### Examples
435
+
436
+ #### Filter by Native Field
437
+
438
+ ```ruby
439
+ results = client.search(
440
+ query: "project update",
441
+ search_method: "chunk_vector_search",
442
+ filters: [
443
+ {
444
+ field: "source",
445
+ operator: "eq",
446
+ value: "notion",
447
+ filter_type: "native_field"
448
+ }
449
+ ]
450
+ )
451
+ ```
452
+
453
+ #### Filter by Tags
454
+
455
+ ```ruby
456
+ results = client.search(
457
+ query: "status",
458
+ search_method: "chunk_vector_search",
459
+ filters: [
460
+ {
461
+ field: "tags",
462
+ operator: "in",
463
+ value: ["project", "important"],
464
+ filter_type: "native_field"
465
+ }
466
+ ]
467
+ )
468
+ ```
469
+
470
+ #### Filter by Custom Metadata
471
+
472
+ ```ruby
473
+ results = client.search(
474
+ query: "urgent tasks",
475
+ search_method: "chunk_vector_search",
476
+ filters: [
477
+ {
478
+ field: "priority",
479
+ operator: "eq",
480
+ value: "high",
481
+ filter_type: "custom_metadata"
482
+ }
483
+ ]
484
+ )
485
+ ```
486
+
487
+ #### Multiple Filters
488
+
489
+ ```ruby
490
+ results = client.search(
491
+ query: "engineering work",
492
+ search_method: "chunk_vector_search",
493
+ filters: [
494
+ {
495
+ field: "source",
496
+ operator: "eq",
497
+ value: "jira",
498
+ filter_type: "native_field"
499
+ },
500
+ {
501
+ field: "tags",
502
+ operator: "in",
503
+ value: ["backend", "api"],
504
+ filter_type: "native_field"
505
+ },
506
+ {
507
+ field: "priority",
508
+ operator: "eq",
509
+ value: "high",
510
+ filter_type: "custom_metadata"
511
+ }
512
+ ]
513
+ )
514
+ ```
515
+
516
+ #### Filters in Chat
517
+
518
+ ```ruby
519
+ response = client.chat(
520
+ query: "What are the high priority backend tasks?",
521
+ filters: [
522
+ {
523
+ field: "department",
524
+ operator: "eq",
525
+ value: "engineering",
526
+ filter_type: "custom_metadata"
527
+ }
528
+ ]
529
+ )
530
+ ```
531
+
532
+ #### Filters in Document Generation
533
+
534
+ ```ruby
535
+ response = client.generate_doc(
536
+ prompt: "Create a security audit summary",
537
+ rules: "Focus on key findings",
538
+ filters: [
539
+ {
540
+ field: "tags",
541
+ operator: "in",
542
+ value: ["security", "audit"],
543
+ filter_type: "native_field"
544
+ }
545
+ ]
546
+ )
547
+ ```
548
+
549
+ ## Error Handling
550
+
551
+ The SDK raises exceptions for API errors. Wrap your calls in begin/rescue blocks:
552
+
553
+ ```ruby
554
+ begin
555
+ memo = client.get_memo("invalid-id")
556
+ rescue => e
557
+ puts "Error: #{e.message}"
558
+ # => "Skald API error (404): Not Found"
559
+ end
560
+ ```
561
+
562
+ All methods may raise `RuntimeError` with the format:
563
+ ```
564
+ Skald API error (STATUS_CODE): ERROR_MESSAGE
565
+ ```
566
+
567
+ ## Examples
568
+
569
+ See the [examples](examples/) directory for complete working examples:
570
+
571
+ - [memo_operations.rb](examples/memo_operations.rb) - Create, read, update, delete memos
572
+ - [search.rb](examples/search.rb) - All search methods and filtering
573
+ - [chat.rb](examples/chat.rb) - Streaming and non-streaming chat
574
+ - [document_generation.rb](examples/document_generation.rb) - Document generation with rules
575
+ - [filters.rb](examples/filters.rb) - Advanced filtering examples
576
+
577
+ ## Development
578
+
579
+ ```bash
580
+ # Install dependencies
581
+ bundle install
582
+
583
+ # Run tests
584
+ bundle exec rspec
585
+
586
+ # Run tests with coverage
587
+ bundle exec rspec --format documentation
588
+
589
+ # Run linter
590
+ bundle exec rubocop
591
+ ```
592
+
593
+ ## Testing
594
+
595
+ The SDK includes comprehensive tests with 100% method coverage:
596
+
597
+ ```bash
598
+ bundle exec rspec
599
+ # => 54 examples, 0 failures
600
+ ```
601
+
602
+ Tests use WebMock to mock HTTP requests, so no actual API calls are made during testing.
603
+
604
+ ## API Reference
605
+
606
+ For complete API documentation, visit the [Skald API Documentation](https://docs.useskald.com).
607
+
608
+ ## Contributing
609
+
610
+ Bug reports and pull requests are welcome on GitHub at https://github.com/skald-org/skald-ruby.
611
+
612
+ ## License
613
+
614
+ The gem is available as open source under the terms of the [MIT License](LICENSE).
615
+
616
+ ## Support
617
+
618
+ - Documentation: https://docs.useskald.com
619
+ - Email: support@useskald.com
620
+ - GitHub Issues: https://github.com/skald-org/skald-ruby/issues
data/lib/skald.rb ADDED
@@ -0,0 +1,335 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+ require "cgi"
7
+
8
+ # Skald Ruby SDK
9
+ #
10
+ # A Ruby client library for interacting with the Skald API platform.
11
+ # Provides methods for managing memos, searching, chatting, and generating documents.
12
+ class Skald
13
+ # Base URL for the Skald API
14
+ DEFAULT_BASE_URL = "https://api.useskald.com"
15
+
16
+ # @return [String] the API key used for authentication
17
+ attr_reader :api_key
18
+
19
+ # @return [String] the base URL for API requests
20
+ attr_reader :base_url
21
+
22
+ # Initialize a new Skald client
23
+ #
24
+ # @param api_key [String] Your Skald API key (required)
25
+ # @param base_url [String] Optional custom base URL (defaults to https://api.useskald.com)
26
+ #
27
+ # @example
28
+ # client = Skald.new("your-api-key")
29
+ # # Or with custom base URL
30
+ # client = Skald.new("your-api-key", "https://custom.api.com")
31
+ def initialize(api_key, base_url = DEFAULT_BASE_URL)
32
+ raise ArgumentError, "API key is required" if api_key.nil? || api_key.empty?
33
+
34
+ @api_key = api_key
35
+ @base_url = base_url.gsub(%r{/+$}, "") # Remove trailing slashes
36
+ end
37
+
38
+ # Create a new memo
39
+ #
40
+ # @param memo_data [Hash] The memo data
41
+ # @option memo_data [String] :title The memo title (required, max 255 chars)
42
+ # @option memo_data [String] :content The memo content (required)
43
+ # @option memo_data [Hash] :metadata Optional custom metadata (JSON object)
44
+ # @option memo_data [String] :reference_id Optional external reference ID (max 255 chars)
45
+ # @option memo_data [Array<String>] :tags Optional array of tags
46
+ # @option memo_data [String] :source Optional source system (e.g., "notion", max 255 chars)
47
+ # @option memo_data [String] :expiration_date Optional ISO 8601 expiration timestamp
48
+ #
49
+ # @return [Hash] Response with :ok key
50
+ #
51
+ # @example
52
+ # result = client.create_memo(
53
+ # title: "Meeting Notes",
54
+ # content: "Discussed project timeline...",
55
+ # metadata: { priority: "high" },
56
+ # tags: ["meeting", "project"]
57
+ # )
58
+ def create_memo(memo_data)
59
+ data = memo_data.dup
60
+ data[:metadata] ||= {}
61
+
62
+ request(:post, "/api/v1/memo", data)
63
+ end
64
+
65
+ # Get a memo by ID
66
+ #
67
+ # @param memo_id [String] The memo UUID or reference ID
68
+ # @param id_type [String] Type of ID: "memo_uuid" or "reference_id" (default: "memo_uuid")
69
+ #
70
+ # @return [Hash] The memo object with all fields
71
+ #
72
+ # @example
73
+ # memo = client.get_memo("550e8400-e29b-41d4-a716-446655440000")
74
+ # # Or by reference ID
75
+ # memo = client.get_memo("my-ref-id", "reference_id")
76
+ def get_memo(memo_id, id_type = "memo_uuid")
77
+ encoded_id = CGI.escape(memo_id)
78
+ params = id_type != "memo_uuid" ? "?id_type=#{id_type}" : ""
79
+ request(:get, "/api/v1/memo/#{encoded_id}#{params}")
80
+ end
81
+
82
+ # List memos with pagination
83
+ #
84
+ # @param params [Hash] Optional pagination parameters
85
+ # @option params [Integer] :page Page number (default: 1)
86
+ # @option params [Integer] :page_size Number of results per page (default: 20, max: 100)
87
+ #
88
+ # @return [Hash] Response with :count, :next, :previous, and :results keys
89
+ #
90
+ # @example
91
+ # response = client.list_memos(page: 1, page_size: 50)
92
+ # response[:results].each do |memo|
93
+ # puts memo[:title]
94
+ # end
95
+ def list_memos(params = {})
96
+ query_params = []
97
+ query_params << "page=#{params[:page]}" if params[:page]
98
+ query_params << "page_size=#{params[:page_size]}" if params[:page_size]
99
+ query_string = query_params.empty? ? "" : "?#{query_params.join('&')}"
100
+
101
+ request(:get, "/api/v1/memo#{query_string}")
102
+ end
103
+
104
+ # Update an existing memo
105
+ #
106
+ # @param memo_id [String] The memo UUID or reference ID
107
+ # @param update_data [Hash] The fields to update
108
+ # @option update_data [String] :title New title
109
+ # @option update_data [String] :content New content (triggers reprocessing)
110
+ # @option update_data [Hash] :metadata New metadata
111
+ # @option update_data [String] :client_reference_id New reference ID
112
+ # @option update_data [String] :source New source
113
+ # @option update_data [String] :expiration_date New expiration date
114
+ # @param id_type [String] Type of ID: "memo_uuid" or "reference_id" (default: "memo_uuid")
115
+ #
116
+ # @return [Hash] Response with :ok key
117
+ #
118
+ # @example
119
+ # client.update_memo(
120
+ # "550e8400-e29b-41d4-a716-446655440000",
121
+ # { title: "Updated Title", metadata: { status: "completed" } }
122
+ # )
123
+ def update_memo(memo_id, update_data, id_type = "memo_uuid")
124
+ encoded_id = CGI.escape(memo_id)
125
+ params = id_type != "memo_uuid" ? "?id_type=#{id_type}" : ""
126
+ request(:patch, "/api/v1/memo/#{encoded_id}#{params}", update_data)
127
+ end
128
+
129
+ # Delete a memo
130
+ #
131
+ # @param memo_id [String] The memo UUID or reference ID
132
+ # @param id_type [String] Type of ID: "memo_uuid" or "reference_id" (default: "memo_uuid")
133
+ #
134
+ # @return [nil]
135
+ #
136
+ # @example
137
+ # client.delete_memo("550e8400-e29b-41d4-a716-446655440000")
138
+ def delete_memo(memo_id, id_type = "memo_uuid")
139
+ encoded_id = CGI.escape(memo_id)
140
+ params = id_type != "memo_uuid" ? "?id_type=#{id_type}" : ""
141
+ request(:delete, "/api/v1/memo/#{encoded_id}#{params}")
142
+ nil
143
+ end
144
+
145
+ # Search for memos
146
+ #
147
+ # @param search_params [Hash] Search parameters
148
+ # @option search_params [String] :query The search query (required)
149
+ # @option search_params [String] :search_method Search method: "chunk_vector_search", "title_contains", or "title_startswith" (required)
150
+ # @option search_params [Integer] :limit Maximum number of results (1-50, default: 10)
151
+ # @option search_params [Array<Hash>] :filters Optional filters to apply
152
+ #
153
+ # @return [Hash] Response with :results array
154
+ #
155
+ # @example
156
+ # results = client.search(
157
+ # query: "project timeline",
158
+ # search_method: "chunk_vector_search",
159
+ # limit: 20
160
+ # )
161
+ def search(search_params)
162
+ request(:post, "/api/v1/search", search_params)
163
+ end
164
+
165
+ # Chat with your memos (non-streaming)
166
+ #
167
+ # @param chat_params [Hash] Chat parameters
168
+ # @option chat_params [String] :query The question to ask (required)
169
+ # @option chat_params [Array<Hash>] :filters Optional filters to apply
170
+ #
171
+ # @return [Hash] Response with :ok, :response, and :intermediate_steps keys
172
+ #
173
+ # @example
174
+ # response = client.chat(query: "What are the main project goals?")
175
+ # puts response[:response]
176
+ def chat(chat_params)
177
+ params = chat_params.dup
178
+ params[:stream] = false
179
+ request(:post, "/api/v1/chat", params)
180
+ end
181
+
182
+ # Chat with your memos (streaming)
183
+ #
184
+ # @param chat_params [Hash] Chat parameters
185
+ # @option chat_params [String] :query The question to ask (required)
186
+ # @option chat_params [Array<Hash>] :filters Optional filters to apply
187
+ #
188
+ # @return [Enumerator] An enumerator that yields events
189
+ #
190
+ # @example
191
+ # client.streamed_chat(query: "Summarize the project").each do |event|
192
+ # if event[:type] == "token"
193
+ # print event[:content]
194
+ # elsif event[:type] == "done"
195
+ # puts "\nDone!"
196
+ # end
197
+ # end
198
+ def streamed_chat(chat_params)
199
+ params = chat_params.dup
200
+ params[:stream] = true
201
+ stream_request(:post, "/api/v1/chat", params)
202
+ end
203
+
204
+ # Generate a document (non-streaming)
205
+ #
206
+ # @param generate_params [Hash] Generation parameters
207
+ # @option generate_params [String] :prompt What document to generate (required)
208
+ # @option generate_params [String] :rules Optional style/format rules
209
+ # @option generate_params [Array<Hash>] :filters Optional filters to apply
210
+ #
211
+ # @return [Hash] Response with :ok, :response, and :intermediate_steps keys
212
+ #
213
+ # @example
214
+ # doc = client.generate_doc(
215
+ # prompt: "Create a project status report",
216
+ # rules: "Use bullet points and be concise"
217
+ # )
218
+ # puts doc[:response]
219
+ def generate_doc(generate_params)
220
+ params = generate_params.dup
221
+ params[:stream] = false
222
+ request(:post, "/api/v1/generate", params)
223
+ end
224
+
225
+ # Generate a document (streaming)
226
+ #
227
+ # @param generate_params [Hash] Generation parameters
228
+ # @option generate_params [String] :prompt What document to generate (required)
229
+ # @option generate_params [String] :rules Optional style/format rules
230
+ # @option generate_params [Array<Hash>] :filters Optional filters to apply
231
+ #
232
+ # @return [Enumerator] An enumerator that yields events
233
+ #
234
+ # @example
235
+ # client.streamed_generate_doc(prompt: "Write a summary").each do |event|
236
+ # if event[:type] == "token"
237
+ # print event[:content]
238
+ # elsif event[:type] == "done"
239
+ # puts "\nDone!"
240
+ # end
241
+ # end
242
+ def streamed_generate_doc(generate_params)
243
+ params = generate_params.dup
244
+ params[:stream] = true
245
+ stream_request(:post, "/api/v1/generate", params)
246
+ end
247
+
248
+ private
249
+
250
+ # Make an HTTP request
251
+ def request(method, path, body = nil)
252
+ uri = URI.join(@base_url, path)
253
+ http = Net::HTTP.new(uri.host, uri.port)
254
+ http.use_ssl = uri.scheme == "https"
255
+
256
+ request_class = case method
257
+ when :get then Net::HTTP::Get
258
+ when :post then Net::HTTP::Post
259
+ when :patch then Net::HTTP::Patch
260
+ when :delete then Net::HTTP::Delete
261
+ else raise ArgumentError, "Unsupported method: #{method}"
262
+ end
263
+
264
+ request = request_class.new(uri.request_uri)
265
+ request["Authorization"] = "Bearer #{@api_key}"
266
+ request["Content-Type"] = "application/json" if body
267
+
268
+ request.body = body.to_json if body
269
+
270
+ response = http.request(request)
271
+
272
+ unless response.is_a?(Net::HTTPSuccess)
273
+ raise "Skald API error (#{response.code}): #{response.body}"
274
+ end
275
+
276
+ # DELETE returns empty response
277
+ return nil if response.body.nil? || response.body.empty?
278
+
279
+ JSON.parse(response.body, symbolize_names: true)
280
+ end
281
+
282
+ # Make a streaming HTTP request
283
+ def stream_request(method, path, body)
284
+ Enumerator.new do |yielder|
285
+ uri = URI.join(@base_url, path)
286
+ http = Net::HTTP.new(uri.host, uri.port)
287
+ http.use_ssl = uri.scheme == "https"
288
+ http.read_timeout = 300 # 5 minutes for streaming
289
+
290
+ request_class = case method
291
+ when :post then Net::HTTP::Post
292
+ else raise ArgumentError, "Unsupported streaming method: #{method}"
293
+ end
294
+
295
+ request = request_class.new(uri.request_uri)
296
+ request["Authorization"] = "Bearer #{@api_key}"
297
+ request["Content-Type"] = "application/json"
298
+ request.body = body.to_json
299
+
300
+ buffer = ""
301
+
302
+ http.request(request) do |response|
303
+ unless response.is_a?(Net::HTTPSuccess)
304
+ raise "Skald API error (#{response.code}): #{response.body}"
305
+ end
306
+
307
+ response.read_body do |chunk|
308
+ buffer += chunk
309
+
310
+ # Process complete lines
311
+ while (newline_index = buffer.index("\n"))
312
+ line = buffer[0...newline_index].strip
313
+ buffer = buffer[(newline_index + 1)..-1] || ""
314
+
315
+ next if line.empty?
316
+ next if line.start_with?(": ") # Skip ping events
317
+
318
+ # Parse SSE format (data: {...})
319
+ if line.start_with?("data: ")
320
+ json_str = line[6..-1]
321
+ begin
322
+ event = JSON.parse(json_str, symbolize_names: true)
323
+ yielder << event
324
+ break if event[:type] == "done"
325
+ rescue JSON::ParserError
326
+ # Skip invalid JSON
327
+ next
328
+ end
329
+ end
330
+ end
331
+ end
332
+ end
333
+ end
334
+ end
335
+ end
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: skald
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Skald
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-10-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.13'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.13'
27
+ - !ruby/object:Gem::Dependency
28
+ name: webmock
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.23'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.23'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.21'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.21'
69
+ description: Ruby client library for Skald - an API platform for context/knowledge
70
+ management with chat, document generation, and semantic search capabilities
71
+ email:
72
+ - support@useskald.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - LICENSE
78
+ - README.md
79
+ - lib/skald.rb
80
+ homepage: https://github.com/skald-org/skald-ruby
81
+ licenses:
82
+ - MIT
83
+ metadata:
84
+ homepage_uri: https://github.com/skald-org/skald-ruby
85
+ source_code_uri: https://github.com/skald-org/skald-ruby
86
+ changelog_uri: https://github.com/skald-org/skald-ruby/blob/main/CHANGELOG.md
87
+ post_install_message:
88
+ rdoc_options: []
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: 3.0.0
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ requirements: []
102
+ rubygems_version: 3.5.22
103
+ signing_key:
104
+ specification_version: 4
105
+ summary: Ruby SDK for Skald API
106
+ test_files: []