tsikol 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +22 -0
- data/CONTRIBUTING.md +84 -0
- data/LICENSE +21 -0
- data/README.md +579 -0
- data/Rakefile +12 -0
- data/docs/README.md +69 -0
- data/docs/api/middleware.md +721 -0
- data/docs/api/prompt.md +858 -0
- data/docs/api/resource.md +651 -0
- data/docs/api/server.md +509 -0
- data/docs/api/test-helpers.md +591 -0
- data/docs/api/tool.md +527 -0
- data/docs/cookbook/authentication.md +651 -0
- data/docs/cookbook/caching.md +877 -0
- data/docs/cookbook/dynamic-tools.md +970 -0
- data/docs/cookbook/error-handling.md +887 -0
- data/docs/cookbook/logging.md +1044 -0
- data/docs/cookbook/rate-limiting.md +717 -0
- data/docs/examples/code-assistant.md +922 -0
- data/docs/examples/complete-server.md +726 -0
- data/docs/examples/database-manager.md +1198 -0
- data/docs/examples/devops-tools.md +1382 -0
- data/docs/examples/echo-server.md +501 -0
- data/docs/examples/weather-service.md +822 -0
- data/docs/guides/completion.md +472 -0
- data/docs/guides/getting-started.md +462 -0
- data/docs/guides/middleware.md +823 -0
- data/docs/guides/project-structure.md +434 -0
- data/docs/guides/prompts.md +920 -0
- data/docs/guides/resources.md +720 -0
- data/docs/guides/sampling.md +804 -0
- data/docs/guides/testing.md +863 -0
- data/docs/guides/tools.md +627 -0
- data/examples/README.md +92 -0
- data/examples/advanced_features.rb +129 -0
- data/examples/basic-migrated/app/prompts/weather_chat.rb +44 -0
- data/examples/basic-migrated/app/resources/weather_alerts.rb +18 -0
- data/examples/basic-migrated/app/tools/get_current_weather.rb +34 -0
- data/examples/basic-migrated/app/tools/get_forecast.rb +30 -0
- data/examples/basic-migrated/app/tools/get_weather_by_coords.rb +48 -0
- data/examples/basic-migrated/server.rb +25 -0
- data/examples/basic.rb +73 -0
- data/examples/full_featured.rb +175 -0
- data/examples/middleware_example.rb +112 -0
- data/examples/sampling_example.rb +104 -0
- data/examples/weather-service/app/prompts/weather/chat.rb +90 -0
- data/examples/weather-service/app/resources/weather/alerts.rb +59 -0
- data/examples/weather-service/app/tools/weather/get_current.rb +82 -0
- data/examples/weather-service/app/tools/weather/get_forecast.rb +90 -0
- data/examples/weather-service/server.rb +28 -0
- data/exe/tsikol +6 -0
- data/lib/tsikol/cli/templates/Gemfile.erb +10 -0
- data/lib/tsikol/cli/templates/README.md.erb +38 -0
- data/lib/tsikol/cli/templates/gitignore.erb +49 -0
- data/lib/tsikol/cli/templates/prompt.rb.erb +53 -0
- data/lib/tsikol/cli/templates/resource.rb.erb +29 -0
- data/lib/tsikol/cli/templates/server.rb.erb +24 -0
- data/lib/tsikol/cli/templates/tool.rb.erb +60 -0
- data/lib/tsikol/cli.rb +203 -0
- data/lib/tsikol/error_handler.rb +141 -0
- data/lib/tsikol/health.rb +198 -0
- data/lib/tsikol/http_transport.rb +72 -0
- data/lib/tsikol/lifecycle.rb +149 -0
- data/lib/tsikol/middleware.rb +168 -0
- data/lib/tsikol/prompt.rb +101 -0
- data/lib/tsikol/resource.rb +53 -0
- data/lib/tsikol/router.rb +190 -0
- data/lib/tsikol/server.rb +660 -0
- data/lib/tsikol/stdio_transport.rb +108 -0
- data/lib/tsikol/test_helpers.rb +261 -0
- data/lib/tsikol/tool.rb +111 -0
- data/lib/tsikol/version.rb +5 -0
- data/lib/tsikol.rb +72 -0
- metadata +219 -0
data/docs/api/tool.md
ADDED
@@ -0,0 +1,527 @@
|
|
1
|
+
# Tool API Reference
|
2
|
+
|
3
|
+
The `Tsikol::Tool` class provides the foundation for creating tools that AI assistants can call.
|
4
|
+
|
5
|
+
## Class: Tsikol::Tool
|
6
|
+
|
7
|
+
### Class Methods
|
8
|
+
|
9
|
+
#### `.description(text = nil)`
|
10
|
+
|
11
|
+
Set or get the tool description.
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
class MyTool < Tsikol::Tool
|
15
|
+
description "Performs useful operations"
|
16
|
+
end
|
17
|
+
```
|
18
|
+
|
19
|
+
#### `.parameter(name, &block)`
|
20
|
+
|
21
|
+
Define a tool parameter with validation and completion.
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
class FileTool < Tsikol::Tool
|
25
|
+
parameter :path do
|
26
|
+
type :string
|
27
|
+
required
|
28
|
+
description "File path"
|
29
|
+
|
30
|
+
complete do |partial|
|
31
|
+
Dir.glob("#{partial}*")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
parameter :options do
|
36
|
+
type :object
|
37
|
+
optional
|
38
|
+
default {}
|
39
|
+
description "Additional options"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
### Parameter DSL
|
45
|
+
|
46
|
+
Parameters are defined using a DSL within the parameter block:
|
47
|
+
|
48
|
+
#### `type(symbol)`
|
49
|
+
|
50
|
+
Set the parameter type.
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
parameter :count do
|
54
|
+
type :integer # :string, :number, :integer, :boolean, :array, :object
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
#### `required` / `optional`
|
59
|
+
|
60
|
+
Specify if parameter is required.
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
parameter :name do
|
64
|
+
type :string
|
65
|
+
required # or optional
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
#### `default(value)`
|
70
|
+
|
71
|
+
Set default value for optional parameters.
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
parameter :format do
|
75
|
+
type :string
|
76
|
+
optional
|
77
|
+
default "json"
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
#### `description(text)`
|
82
|
+
|
83
|
+
Describe the parameter's purpose.
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
parameter :query do
|
87
|
+
type :string
|
88
|
+
required
|
89
|
+
description "Search query string"
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
#### `enum(values)`
|
94
|
+
|
95
|
+
Restrict parameter to specific values.
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
parameter :operation do
|
99
|
+
type :string
|
100
|
+
required
|
101
|
+
enum ["read", "write", "delete"]
|
102
|
+
description "File operation"
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
#### `complete(&block)`
|
107
|
+
|
108
|
+
Define completion logic.
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
parameter :language do
|
112
|
+
type :string
|
113
|
+
required
|
114
|
+
|
115
|
+
complete do |partial|
|
116
|
+
languages = ["ruby", "python", "javascript", "go"]
|
117
|
+
languages.select { |lang| lang.start_with?(partial) }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
122
|
+
### Instance Methods
|
123
|
+
|
124
|
+
#### `#execute(**params)`
|
125
|
+
|
126
|
+
Main method to implement tool logic. Override in subclasses.
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
class Calculator < Tsikol::Tool
|
130
|
+
parameter :a do
|
131
|
+
type :number
|
132
|
+
required
|
133
|
+
end
|
134
|
+
|
135
|
+
parameter :b do
|
136
|
+
type :number
|
137
|
+
required
|
138
|
+
end
|
139
|
+
|
140
|
+
parameter :operation do
|
141
|
+
type :string
|
142
|
+
required
|
143
|
+
enum ["add", "subtract", "multiply", "divide"]
|
144
|
+
end
|
145
|
+
|
146
|
+
def execute(a:, b:, operation:)
|
147
|
+
case operation
|
148
|
+
when "add" then a + b
|
149
|
+
when "subtract" then a - b
|
150
|
+
when "multiply" then a * b
|
151
|
+
when "divide"
|
152
|
+
raise Tsikol::ValidationError, "Division by zero" if b == 0
|
153
|
+
a.to_f / b
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
```
|
158
|
+
|
159
|
+
#### `#set_server(server)`
|
160
|
+
|
161
|
+
Called when tool is registered with a server. Override to access server features.
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
def set_server(server)
|
165
|
+
@server = server
|
166
|
+
|
167
|
+
# Enable logging if server supports it
|
168
|
+
if @server.logging_enabled?
|
169
|
+
define_singleton_method(:log) do |level, message, data: nil|
|
170
|
+
@server.log(level, message, data: data)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
```
|
175
|
+
|
176
|
+
#### `#log(level, message, data: nil)`
|
177
|
+
|
178
|
+
Log messages (only available after server registration with logging enabled).
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
def execute(input:)
|
182
|
+
log :info, "Processing started", input_size: input.size
|
183
|
+
|
184
|
+
result = process(input)
|
185
|
+
|
186
|
+
log :info, "Processing complete", result_size: result.size
|
187
|
+
|
188
|
+
result
|
189
|
+
end
|
190
|
+
```
|
191
|
+
|
192
|
+
### Tool Registration
|
193
|
+
|
194
|
+
Tools can be registered in several ways:
|
195
|
+
|
196
|
+
#### Class Registration
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
server.register_tool_class(MyTool)
|
200
|
+
# or
|
201
|
+
server.tool MyTool
|
202
|
+
```
|
203
|
+
|
204
|
+
#### Instance Registration
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
tool = MyTool.new
|
208
|
+
server.register_tool_instance(tool)
|
209
|
+
```
|
210
|
+
|
211
|
+
#### Inline Registration
|
212
|
+
|
213
|
+
```ruby
|
214
|
+
server.register_tool("simple_tool") do |param:|
|
215
|
+
"Result: #{param}"
|
216
|
+
end
|
217
|
+
# or
|
218
|
+
server.tool "simple_tool" do |param:|
|
219
|
+
"Result: #{param}"
|
220
|
+
end
|
221
|
+
```
|
222
|
+
|
223
|
+
### Parameter Validation
|
224
|
+
|
225
|
+
Parameters are automatically validated:
|
226
|
+
|
227
|
+
```ruby
|
228
|
+
# Type validation
|
229
|
+
parameter :age do
|
230
|
+
type :integer
|
231
|
+
required
|
232
|
+
end
|
233
|
+
# Validates that age is an integer
|
234
|
+
|
235
|
+
# Enum validation
|
236
|
+
parameter :color do
|
237
|
+
type :string
|
238
|
+
enum ["red", "green", "blue"]
|
239
|
+
end
|
240
|
+
# Validates color is one of the allowed values
|
241
|
+
|
242
|
+
# Custom validation in execute
|
243
|
+
def execute(email:)
|
244
|
+
unless email =~ /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
|
245
|
+
raise Tsikol::ValidationError, "Invalid email format"
|
246
|
+
end
|
247
|
+
|
248
|
+
process_email(email)
|
249
|
+
end
|
250
|
+
```
|
251
|
+
|
252
|
+
### Error Handling
|
253
|
+
|
254
|
+
Tools should handle errors gracefully:
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
class SafeTool < Tsikol::Tool
|
258
|
+
def execute(input:)
|
259
|
+
validate_input!(input)
|
260
|
+
result = process(input)
|
261
|
+
format_result(result)
|
262
|
+
|
263
|
+
rescue ValidationError => e
|
264
|
+
# Return user-friendly error
|
265
|
+
"Validation failed: #{e.message}"
|
266
|
+
|
267
|
+
rescue NetworkError => e
|
268
|
+
# Log and return error
|
269
|
+
log :error, "Network error", error: e.message
|
270
|
+
"Network error occurred. Please try again."
|
271
|
+
|
272
|
+
rescue => e
|
273
|
+
# Unexpected errors
|
274
|
+
log :error, "Unexpected error", error: e.class.name, message: e.message
|
275
|
+
"An unexpected error occurred"
|
276
|
+
end
|
277
|
+
end
|
278
|
+
```
|
279
|
+
|
280
|
+
### Async Operations
|
281
|
+
|
282
|
+
Tools can perform async operations:
|
283
|
+
|
284
|
+
```ruby
|
285
|
+
class AsyncTool < Tsikol::Tool
|
286
|
+
def execute(data:)
|
287
|
+
job_id = SecureRandom.uuid
|
288
|
+
|
289
|
+
# Start async processing
|
290
|
+
Thread.new do
|
291
|
+
begin
|
292
|
+
result = long_running_operation(data)
|
293
|
+
store_result(job_id, result)
|
294
|
+
rescue => e
|
295
|
+
store_error(job_id, e)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
# Return immediately
|
300
|
+
{
|
301
|
+
job_id: job_id,
|
302
|
+
status: "processing",
|
303
|
+
check_back_in: 60
|
304
|
+
}.to_json
|
305
|
+
end
|
306
|
+
end
|
307
|
+
```
|
308
|
+
|
309
|
+
### Using Server Capabilities
|
310
|
+
|
311
|
+
Tools can use server features when available:
|
312
|
+
|
313
|
+
```ruby
|
314
|
+
class AiTool < Tsikol::Tool
|
315
|
+
def execute(prompt:)
|
316
|
+
unless @server.sampling_enabled?
|
317
|
+
return "AI features not available"
|
318
|
+
end
|
319
|
+
|
320
|
+
response = @server.sample_text(
|
321
|
+
messages: [
|
322
|
+
{ role: "user", content: { type: "text", text: prompt } }
|
323
|
+
],
|
324
|
+
temperature: 0.7
|
325
|
+
)
|
326
|
+
|
327
|
+
if response[:error]
|
328
|
+
"AI error: #{response[:error]}"
|
329
|
+
else
|
330
|
+
response[:text]
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
```
|
335
|
+
|
336
|
+
## Complete Example
|
337
|
+
|
338
|
+
```ruby
|
339
|
+
class FileManager < Tsikol::Tool
|
340
|
+
description "Manage files and directories"
|
341
|
+
|
342
|
+
parameter :operation do
|
343
|
+
type :string
|
344
|
+
required
|
345
|
+
enum ["read", "write", "list", "delete", "info"]
|
346
|
+
description "Operation to perform"
|
347
|
+
end
|
348
|
+
|
349
|
+
parameter :path do
|
350
|
+
type :string
|
351
|
+
required
|
352
|
+
description "File or directory path"
|
353
|
+
|
354
|
+
complete do |partial|
|
355
|
+
Dir.glob("#{partial}*").map do |path|
|
356
|
+
File.directory?(path) ? "#{path}/" : path
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
parameter :content do
|
362
|
+
type :string
|
363
|
+
optional
|
364
|
+
description "Content for write operations"
|
365
|
+
end
|
366
|
+
|
367
|
+
parameter :options do
|
368
|
+
type :object
|
369
|
+
optional
|
370
|
+
default {}
|
371
|
+
description "Additional options (encoding, etc.)"
|
372
|
+
end
|
373
|
+
|
374
|
+
def execute(operation:, path:, content: nil, options: {})
|
375
|
+
log :info, "File operation", operation: operation, path: path
|
376
|
+
|
377
|
+
case operation
|
378
|
+
when "read"
|
379
|
+
read_file(path, options)
|
380
|
+
when "write"
|
381
|
+
write_file(path, content, options)
|
382
|
+
when "list"
|
383
|
+
list_directory(path)
|
384
|
+
when "delete"
|
385
|
+
delete_file(path)
|
386
|
+
when "info"
|
387
|
+
file_info(path)
|
388
|
+
end
|
389
|
+
|
390
|
+
rescue Errno::ENOENT
|
391
|
+
raise Tsikol::ValidationError, "File not found: #{path}"
|
392
|
+
rescue Errno::EACCES
|
393
|
+
raise Tsikol::ValidationError, "Permission denied: #{path}"
|
394
|
+
rescue => e
|
395
|
+
log :error, "File operation failed", error: e.message
|
396
|
+
raise
|
397
|
+
end
|
398
|
+
|
399
|
+
private
|
400
|
+
|
401
|
+
def read_file(path, options)
|
402
|
+
encoding = options["encoding"] || "UTF-8"
|
403
|
+
File.read(path, encoding: encoding)
|
404
|
+
end
|
405
|
+
|
406
|
+
def write_file(path, content, options)
|
407
|
+
raise Tsikol::ValidationError, "Content required for write" unless content
|
408
|
+
|
409
|
+
mode = options["append"] ? "a" : "w"
|
410
|
+
File.open(path, mode) { |f| f.write(content) }
|
411
|
+
|
412
|
+
"Wrote #{content.bytesize} bytes to #{path}"
|
413
|
+
end
|
414
|
+
|
415
|
+
def list_directory(path)
|
416
|
+
unless File.directory?(path)
|
417
|
+
raise Tsikol::ValidationError, "Not a directory: #{path}"
|
418
|
+
end
|
419
|
+
|
420
|
+
entries = Dir.entries(path).reject { |e| e.start_with?(".") }
|
421
|
+
entries.map do |entry|
|
422
|
+
full_path = File.join(path, entry)
|
423
|
+
{
|
424
|
+
name: entry,
|
425
|
+
type: File.directory?(full_path) ? "directory" : "file",
|
426
|
+
size: File.size(full_path)
|
427
|
+
}
|
428
|
+
end.to_json
|
429
|
+
end
|
430
|
+
|
431
|
+
def delete_file(path)
|
432
|
+
if File.directory?(path)
|
433
|
+
Dir.rmdir(path)
|
434
|
+
"Deleted directory: #{path}"
|
435
|
+
else
|
436
|
+
File.delete(path)
|
437
|
+
"Deleted file: #{path}"
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
def file_info(path)
|
442
|
+
stat = File.stat(path)
|
443
|
+
{
|
444
|
+
path: path,
|
445
|
+
type: stat.directory? ? "directory" : "file",
|
446
|
+
size: stat.size,
|
447
|
+
modified: stat.mtime.iso8601,
|
448
|
+
permissions: stat.mode.to_s(8)
|
449
|
+
}.to_json
|
450
|
+
end
|
451
|
+
|
452
|
+
def set_server(server)
|
453
|
+
@server = server
|
454
|
+
|
455
|
+
if @server.logging_enabled?
|
456
|
+
define_singleton_method(:log) do |level, message, data: nil|
|
457
|
+
@server.log(level, message, data: data)
|
458
|
+
end
|
459
|
+
else
|
460
|
+
define_singleton_method(:log) do |*args|
|
461
|
+
# No-op if logging not enabled
|
462
|
+
end
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end
|
466
|
+
```
|
467
|
+
|
468
|
+
## Testing Tools
|
469
|
+
|
470
|
+
```ruby
|
471
|
+
require 'minitest/autorun'
|
472
|
+
require 'tsikol/test_helpers'
|
473
|
+
|
474
|
+
class FileManagerTest < Minitest::Test
|
475
|
+
include Tsikol::TestHelpers::Assertions
|
476
|
+
|
477
|
+
def setup
|
478
|
+
@server = Tsikol::Server.new(name: "test")
|
479
|
+
@server.logging true
|
480
|
+
@server.register_tool_instance(FileManager.new)
|
481
|
+
@client = Tsikol::TestHelpers::TestClient.new(@server)
|
482
|
+
@client.initialize_connection
|
483
|
+
end
|
484
|
+
|
485
|
+
def test_read_file
|
486
|
+
File.write("test.txt", "Hello, World!")
|
487
|
+
|
488
|
+
response = @client.call_tool("file_manager", {
|
489
|
+
"operation" => "read",
|
490
|
+
"path" => "test.txt"
|
491
|
+
})
|
492
|
+
|
493
|
+
assert_successful_response(response)
|
494
|
+
assert_equal "Hello, World!", response.dig(:result, :content, 0, :text)
|
495
|
+
ensure
|
496
|
+
File.delete("test.txt") if File.exist?("test.txt")
|
497
|
+
end
|
498
|
+
|
499
|
+
def test_parameter_validation
|
500
|
+
response = @client.call_tool("file_manager", {
|
501
|
+
"operation" => "invalid_op",
|
502
|
+
"path" => "test.txt"
|
503
|
+
})
|
504
|
+
|
505
|
+
assert_error_response(response, -32602)
|
506
|
+
assert_match /must be one of/, response[:error][:message]
|
507
|
+
end
|
508
|
+
|
509
|
+
def test_completion
|
510
|
+
response = @client.complete(
|
511
|
+
{ type: "ref/tool", name: "file_manager" },
|
512
|
+
{ name: "path", value: "te" }
|
513
|
+
)
|
514
|
+
|
515
|
+
assert_successful_response(response)
|
516
|
+
values = response.dig(:result, :completion, :values)
|
517
|
+
assert values.any? { |v| v.start_with?("te") }
|
518
|
+
end
|
519
|
+
end
|
520
|
+
```
|
521
|
+
|
522
|
+
## See Also
|
523
|
+
|
524
|
+
- [Tools Guide](../guides/tools.md)
|
525
|
+
- [Server API](server.md)
|
526
|
+
- [Testing Guide](../guides/testing.md)
|
527
|
+
- [Error Handling](../cookbook/error-handling.md)
|