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/prompt.md
ADDED
@@ -0,0 +1,858 @@
|
|
1
|
+
# Prompt API Reference
|
2
|
+
|
3
|
+
The `Tsikol::Prompt` class provides the foundation for creating reusable AI assistant configurations with dynamic arguments.
|
4
|
+
|
5
|
+
## Class: Tsikol::Prompt
|
6
|
+
|
7
|
+
### Class Methods
|
8
|
+
|
9
|
+
#### `.name(value = nil)`
|
10
|
+
|
11
|
+
Set or get the prompt name.
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
class CodeReviewer < Tsikol::Prompt
|
15
|
+
name "code_reviewer"
|
16
|
+
end
|
17
|
+
```
|
18
|
+
|
19
|
+
#### `.description(text = nil)`
|
20
|
+
|
21
|
+
Set or get the prompt description.
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
class CodeReviewer < Tsikol::Prompt
|
25
|
+
name "code_reviewer"
|
26
|
+
description "AI code review assistant for various programming languages"
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
#### `.argument(name, &block)`
|
31
|
+
|
32
|
+
Define a prompt argument with validation and completion.
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
class TranslationPrompt < Tsikol::Prompt
|
36
|
+
argument :text do
|
37
|
+
type :string
|
38
|
+
required
|
39
|
+
description "Text to translate"
|
40
|
+
end
|
41
|
+
|
42
|
+
argument :target_language do
|
43
|
+
type :string
|
44
|
+
required
|
45
|
+
description "Target language for translation"
|
46
|
+
|
47
|
+
complete do |partial|
|
48
|
+
languages = ["spanish", "french", "german", "italian", "portuguese"]
|
49
|
+
languages.select { |lang| lang.start_with?(partial.downcase) }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
argument :style do
|
54
|
+
type :string
|
55
|
+
optional
|
56
|
+
default "formal"
|
57
|
+
enum ["formal", "casual", "technical"]
|
58
|
+
description "Translation style"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
### Argument DSL
|
64
|
+
|
65
|
+
Arguments use the same DSL as tool parameters:
|
66
|
+
|
67
|
+
#### `type(symbol)`
|
68
|
+
|
69
|
+
Set the argument type.
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
argument :count do
|
73
|
+
type :integer # :string, :number, :integer, :boolean, :array, :object
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
#### `required` / `optional`
|
78
|
+
|
79
|
+
Specify if argument is required.
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
argument :topic do
|
83
|
+
type :string
|
84
|
+
required # or optional
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
88
|
+
#### `default(value)`
|
89
|
+
|
90
|
+
Set default value for optional arguments.
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
argument :temperature do
|
94
|
+
type :number
|
95
|
+
optional
|
96
|
+
default 0.7
|
97
|
+
end
|
98
|
+
```
|
99
|
+
|
100
|
+
#### `description(text)`
|
101
|
+
|
102
|
+
Describe the argument's purpose.
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
argument :context do
|
106
|
+
type :string
|
107
|
+
required
|
108
|
+
description "Background context for the AI"
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
#### `enum(values)`
|
113
|
+
|
114
|
+
Restrict argument to specific values.
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
argument :language do
|
118
|
+
type :string
|
119
|
+
required
|
120
|
+
enum ["ruby", "python", "javascript", "go"]
|
121
|
+
description "Programming language"
|
122
|
+
end
|
123
|
+
```
|
124
|
+
|
125
|
+
#### `complete(&block)`
|
126
|
+
|
127
|
+
Define completion logic.
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
argument :file_path do
|
131
|
+
type :string
|
132
|
+
required
|
133
|
+
|
134
|
+
complete do |partial|
|
135
|
+
Dir.glob("#{partial}*").select { |f| File.file?(f) }
|
136
|
+
end
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
### Instance Methods
|
141
|
+
|
142
|
+
#### `#get_messages(**args)`
|
143
|
+
|
144
|
+
Main method to implement prompt logic. Must return an array of message objects.
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
class SimplePrompt < Tsikol::Prompt
|
148
|
+
name "simple_prompt"
|
149
|
+
|
150
|
+
argument :task do
|
151
|
+
type :string
|
152
|
+
required
|
153
|
+
end
|
154
|
+
|
155
|
+
def get_messages(task:)
|
156
|
+
[
|
157
|
+
{
|
158
|
+
role: "system",
|
159
|
+
content: {
|
160
|
+
type: "text",
|
161
|
+
text: "You are a helpful assistant."
|
162
|
+
}
|
163
|
+
},
|
164
|
+
{
|
165
|
+
role: "user",
|
166
|
+
content: {
|
167
|
+
type: "text",
|
168
|
+
text: task
|
169
|
+
}
|
170
|
+
}
|
171
|
+
]
|
172
|
+
end
|
173
|
+
end
|
174
|
+
```
|
175
|
+
|
176
|
+
#### `#name`
|
177
|
+
|
178
|
+
Get the prompt name (instance method).
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
prompt = CodeReviewer.new
|
182
|
+
prompt.name # => "code_reviewer"
|
183
|
+
```
|
184
|
+
|
185
|
+
#### `#set_server(server)`
|
186
|
+
|
187
|
+
Called when prompt is registered with a server. Override to access server features.
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
def set_server(server)
|
191
|
+
@server = server
|
192
|
+
|
193
|
+
# Access server resources if needed
|
194
|
+
if @server.resources["config"]
|
195
|
+
@config = JSON.parse(@server.resources["config"].read)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
```
|
199
|
+
|
200
|
+
### Prompt Registration
|
201
|
+
|
202
|
+
Prompts can be registered in several ways:
|
203
|
+
|
204
|
+
#### Class Registration
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
server.register_prompt_class(CodeReviewer)
|
208
|
+
# or
|
209
|
+
server.prompt CodeReviewer
|
210
|
+
```
|
211
|
+
|
212
|
+
#### Instance Registration
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
prompt = CodeReviewer.new
|
216
|
+
server.register_prompt_instance(prompt)
|
217
|
+
```
|
218
|
+
|
219
|
+
#### Inline Registration
|
220
|
+
|
221
|
+
```ruby
|
222
|
+
server.register_prompt("simple") do |message:|
|
223
|
+
[
|
224
|
+
{
|
225
|
+
role: "user",
|
226
|
+
content: { type: "text", text: message }
|
227
|
+
}
|
228
|
+
]
|
229
|
+
end
|
230
|
+
# or
|
231
|
+
server.prompt "simple" do |message:|
|
232
|
+
[
|
233
|
+
{
|
234
|
+
role: "user",
|
235
|
+
content: { type: "text", text: message }
|
236
|
+
}
|
237
|
+
]
|
238
|
+
end
|
239
|
+
```
|
240
|
+
|
241
|
+
### Message Format
|
242
|
+
|
243
|
+
Messages follow the OpenAI/Anthropic format:
|
244
|
+
|
245
|
+
#### Basic Text Message
|
246
|
+
|
247
|
+
```ruby
|
248
|
+
{
|
249
|
+
role: "system", # or "user", "assistant"
|
250
|
+
content: {
|
251
|
+
type: "text",
|
252
|
+
text: "The message content"
|
253
|
+
}
|
254
|
+
}
|
255
|
+
```
|
256
|
+
|
257
|
+
#### Multiple Content Types
|
258
|
+
|
259
|
+
```ruby
|
260
|
+
{
|
261
|
+
role: "user",
|
262
|
+
content: [
|
263
|
+
{
|
264
|
+
type: "text",
|
265
|
+
text: "What's in this image?"
|
266
|
+
},
|
267
|
+
{
|
268
|
+
type: "image",
|
269
|
+
data: Base64.encode64(image_data),
|
270
|
+
mimeType: "image/png"
|
271
|
+
}
|
272
|
+
]
|
273
|
+
}
|
274
|
+
```
|
275
|
+
|
276
|
+
### Complex Prompt Examples
|
277
|
+
|
278
|
+
#### Multi-Turn Conversation
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
class ConversationalPrompt < Tsikol::Prompt
|
282
|
+
name "conversation"
|
283
|
+
description "Multi-turn conversational AI"
|
284
|
+
|
285
|
+
argument :topic do
|
286
|
+
type :string
|
287
|
+
required
|
288
|
+
description "Conversation topic"
|
289
|
+
end
|
290
|
+
|
291
|
+
argument :history do
|
292
|
+
type :array
|
293
|
+
optional
|
294
|
+
default []
|
295
|
+
description "Previous conversation messages"
|
296
|
+
end
|
297
|
+
|
298
|
+
argument :personality do
|
299
|
+
type :string
|
300
|
+
optional
|
301
|
+
default "helpful"
|
302
|
+
enum ["helpful", "professional", "friendly", "concise"]
|
303
|
+
end
|
304
|
+
|
305
|
+
def get_messages(topic:, history: [], personality: "helpful")
|
306
|
+
messages = []
|
307
|
+
|
308
|
+
# System message with personality
|
309
|
+
messages << {
|
310
|
+
role: "system",
|
311
|
+
content: {
|
312
|
+
type: "text",
|
313
|
+
text: build_system_prompt(personality)
|
314
|
+
}
|
315
|
+
}
|
316
|
+
|
317
|
+
# Include conversation history
|
318
|
+
history.each do |msg|
|
319
|
+
messages << format_history_message(msg)
|
320
|
+
end
|
321
|
+
|
322
|
+
# Current user message
|
323
|
+
messages << {
|
324
|
+
role: "user",
|
325
|
+
content: {
|
326
|
+
type: "text",
|
327
|
+
text: topic
|
328
|
+
}
|
329
|
+
}
|
330
|
+
|
331
|
+
messages
|
332
|
+
end
|
333
|
+
|
334
|
+
private
|
335
|
+
|
336
|
+
def build_system_prompt(personality)
|
337
|
+
base = "You are a conversational AI assistant."
|
338
|
+
|
339
|
+
case personality
|
340
|
+
when "professional"
|
341
|
+
"#{base} Maintain a professional, business-appropriate tone."
|
342
|
+
when "friendly"
|
343
|
+
"#{base} Be warm, friendly, and conversational."
|
344
|
+
when "concise"
|
345
|
+
"#{base} Provide brief, direct responses."
|
346
|
+
else
|
347
|
+
"#{base} Be helpful and thorough in your responses."
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
def format_history_message(msg)
|
352
|
+
{
|
353
|
+
role: msg["role"] || "user",
|
354
|
+
content: {
|
355
|
+
type: "text",
|
356
|
+
text: msg["content"] || msg["text"]
|
357
|
+
}
|
358
|
+
}
|
359
|
+
end
|
360
|
+
end
|
361
|
+
```
|
362
|
+
|
363
|
+
#### Dynamic Context Loading
|
364
|
+
|
365
|
+
```ruby
|
366
|
+
class ContextAwarePrompt < Tsikol::Prompt
|
367
|
+
name "context_aware"
|
368
|
+
description "AI with dynamic context loading"
|
369
|
+
|
370
|
+
argument :query do
|
371
|
+
type :string
|
372
|
+
required
|
373
|
+
description "User query"
|
374
|
+
end
|
375
|
+
|
376
|
+
argument :context_sources do
|
377
|
+
type :array
|
378
|
+
optional
|
379
|
+
default ["docs", "code"]
|
380
|
+
description "Sources to load context from"
|
381
|
+
end
|
382
|
+
|
383
|
+
argument :max_context_length do
|
384
|
+
type :integer
|
385
|
+
optional
|
386
|
+
default 2000
|
387
|
+
description "Maximum context length in tokens"
|
388
|
+
end
|
389
|
+
|
390
|
+
def get_messages(query:, context_sources: ["docs", "code"], max_context_length: 2000)
|
391
|
+
messages = []
|
392
|
+
|
393
|
+
# System message
|
394
|
+
messages << {
|
395
|
+
role: "system",
|
396
|
+
content: {
|
397
|
+
type: "text",
|
398
|
+
text: "You are an AI assistant with access to project documentation and code."
|
399
|
+
}
|
400
|
+
}
|
401
|
+
|
402
|
+
# Load and add context
|
403
|
+
context_sources.each do |source|
|
404
|
+
context = load_context(source, query)
|
405
|
+
next if context.empty?
|
406
|
+
|
407
|
+
messages << {
|
408
|
+
role: "system",
|
409
|
+
content: {
|
410
|
+
type: "text",
|
411
|
+
text: "Context from #{source}:\n#{truncate(context, max_context_length)}"
|
412
|
+
}
|
413
|
+
}
|
414
|
+
end
|
415
|
+
|
416
|
+
# User query
|
417
|
+
messages << {
|
418
|
+
role: "user",
|
419
|
+
content: {
|
420
|
+
type: "text",
|
421
|
+
text: query
|
422
|
+
}
|
423
|
+
}
|
424
|
+
|
425
|
+
messages
|
426
|
+
end
|
427
|
+
|
428
|
+
private
|
429
|
+
|
430
|
+
def load_context(source, query)
|
431
|
+
case source
|
432
|
+
when "docs"
|
433
|
+
load_documentation_context(query)
|
434
|
+
when "code"
|
435
|
+
load_code_context(query)
|
436
|
+
when "data"
|
437
|
+
load_data_context(query)
|
438
|
+
else
|
439
|
+
""
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
def load_documentation_context(query)
|
444
|
+
# Search documentation for relevant content
|
445
|
+
relevant_docs = search_docs(query)
|
446
|
+
relevant_docs.map { |doc| "#{doc[:title]}:\n#{doc[:content]}" }.join("\n\n")
|
447
|
+
end
|
448
|
+
|
449
|
+
def load_code_context(query)
|
450
|
+
# Search codebase for relevant code
|
451
|
+
relevant_files = search_code(query)
|
452
|
+
relevant_files.map { |file| "File: #{file[:path]}\n```\n#{file[:content]}\n```" }.join("\n\n")
|
453
|
+
end
|
454
|
+
|
455
|
+
def truncate(text, max_length)
|
456
|
+
return text if text.length <= max_length
|
457
|
+
text[0...max_length] + "... (truncated)"
|
458
|
+
end
|
459
|
+
end
|
460
|
+
```
|
461
|
+
|
462
|
+
#### Template-Based Prompts
|
463
|
+
|
464
|
+
```ruby
|
465
|
+
class TemplatePrompt < Tsikol::Prompt
|
466
|
+
name "template"
|
467
|
+
description "Template-based prompt generation"
|
468
|
+
|
469
|
+
argument :template_name do
|
470
|
+
type :string
|
471
|
+
required
|
472
|
+
enum ["bug_report", "feature_request", "code_review", "documentation"]
|
473
|
+
description "Template to use"
|
474
|
+
end
|
475
|
+
|
476
|
+
argument :variables do
|
477
|
+
type :object
|
478
|
+
required
|
479
|
+
description "Variables to fill in the template"
|
480
|
+
end
|
481
|
+
|
482
|
+
def get_messages(template_name:, variables:)
|
483
|
+
template = load_template(template_name)
|
484
|
+
filled_template = fill_template(template, variables)
|
485
|
+
|
486
|
+
[
|
487
|
+
{
|
488
|
+
role: "system",
|
489
|
+
content: {
|
490
|
+
type: "text",
|
491
|
+
text: template[:system_prompt]
|
492
|
+
}
|
493
|
+
},
|
494
|
+
{
|
495
|
+
role: "user",
|
496
|
+
content: {
|
497
|
+
type: "text",
|
498
|
+
text: filled_template
|
499
|
+
}
|
500
|
+
}
|
501
|
+
]
|
502
|
+
end
|
503
|
+
|
504
|
+
private
|
505
|
+
|
506
|
+
def load_template(name)
|
507
|
+
case name
|
508
|
+
when "bug_report"
|
509
|
+
{
|
510
|
+
system_prompt: "You are a software engineer analyzing bug reports. Provide detailed analysis and potential solutions.",
|
511
|
+
template: <<~TEMPLATE
|
512
|
+
Bug Report Analysis Request
|
513
|
+
|
514
|
+
Title: {{title}}
|
515
|
+
Severity: {{severity}}
|
516
|
+
Component: {{component}}
|
517
|
+
|
518
|
+
Description:
|
519
|
+
{{description}}
|
520
|
+
|
521
|
+
Steps to Reproduce:
|
522
|
+
{{steps}}
|
523
|
+
|
524
|
+
Expected Behavior:
|
525
|
+
{{expected}}
|
526
|
+
|
527
|
+
Actual Behavior:
|
528
|
+
{{actual}}
|
529
|
+
|
530
|
+
Environment:
|
531
|
+
{{environment}}
|
532
|
+
TEMPLATE
|
533
|
+
}
|
534
|
+
when "code_review"
|
535
|
+
{
|
536
|
+
system_prompt: "You are an experienced code reviewer. Provide constructive feedback on code quality, best practices, and potential improvements.",
|
537
|
+
template: <<~TEMPLATE
|
538
|
+
Code Review Request
|
539
|
+
|
540
|
+
Language: {{language}}
|
541
|
+
Type: {{change_type}}
|
542
|
+
|
543
|
+
Description of Changes:
|
544
|
+
{{description}}
|
545
|
+
|
546
|
+
Code:
|
547
|
+
```{{language}}
|
548
|
+
{{code}}
|
549
|
+
```
|
550
|
+
|
551
|
+
Specific Concerns:
|
552
|
+
{{concerns}}
|
553
|
+
TEMPLATE
|
554
|
+
}
|
555
|
+
# ... other templates
|
556
|
+
end
|
557
|
+
end
|
558
|
+
|
559
|
+
def fill_template(template, variables)
|
560
|
+
result = template[:template].dup
|
561
|
+
|
562
|
+
variables.each do |key, value|
|
563
|
+
result.gsub!("{{#{key}}}", value.to_s)
|
564
|
+
end
|
565
|
+
|
566
|
+
# Check for missing variables
|
567
|
+
missing = result.scan(/\{\{(\w+)\}\}/).flatten
|
568
|
+
unless missing.empty?
|
569
|
+
raise Tsikol::ValidationError, "Missing template variables: #{missing.join(', ')}"
|
570
|
+
end
|
571
|
+
|
572
|
+
result
|
573
|
+
end
|
574
|
+
end
|
575
|
+
```
|
576
|
+
|
577
|
+
### Prompt Composition
|
578
|
+
|
579
|
+
Combine multiple prompts or behaviors:
|
580
|
+
|
581
|
+
```ruby
|
582
|
+
class CompositePrompt < Tsikol::Prompt
|
583
|
+
name "composite"
|
584
|
+
description "Combines multiple AI behaviors"
|
585
|
+
|
586
|
+
argument :task do
|
587
|
+
type :string
|
588
|
+
required
|
589
|
+
end
|
590
|
+
|
591
|
+
argument :modes do
|
592
|
+
type :array
|
593
|
+
required
|
594
|
+
description "AI modes to activate"
|
595
|
+
end
|
596
|
+
|
597
|
+
argument :context do
|
598
|
+
type :object
|
599
|
+
optional
|
600
|
+
default {}
|
601
|
+
end
|
602
|
+
|
603
|
+
def get_messages(task:, modes:, context: {})
|
604
|
+
messages = []
|
605
|
+
|
606
|
+
# Base system message
|
607
|
+
messages << {
|
608
|
+
role: "system",
|
609
|
+
content: {
|
610
|
+
type: "text",
|
611
|
+
text: "You are a multi-skilled AI assistant."
|
612
|
+
}
|
613
|
+
}
|
614
|
+
|
615
|
+
# Add mode-specific instructions
|
616
|
+
modes.each do |mode|
|
617
|
+
mode_messages = get_mode_messages(mode, context)
|
618
|
+
messages.concat(mode_messages)
|
619
|
+
end
|
620
|
+
|
621
|
+
# Add the task
|
622
|
+
messages << {
|
623
|
+
role: "user",
|
624
|
+
content: {
|
625
|
+
type: "text",
|
626
|
+
text: task
|
627
|
+
}
|
628
|
+
}
|
629
|
+
|
630
|
+
messages
|
631
|
+
end
|
632
|
+
|
633
|
+
private
|
634
|
+
|
635
|
+
def get_mode_messages(mode, context)
|
636
|
+
case mode
|
637
|
+
when "researcher"
|
638
|
+
researcher_messages(context)
|
639
|
+
when "coder"
|
640
|
+
coder_messages(context)
|
641
|
+
when "analyst"
|
642
|
+
analyst_messages(context)
|
643
|
+
else
|
644
|
+
[]
|
645
|
+
end
|
646
|
+
end
|
647
|
+
|
648
|
+
def researcher_messages(context)
|
649
|
+
[{
|
650
|
+
role: "system",
|
651
|
+
content: {
|
652
|
+
type: "text",
|
653
|
+
text: <<~PROMPT
|
654
|
+
As a researcher:
|
655
|
+
- Gather information from multiple sources
|
656
|
+
- Verify facts and cite sources
|
657
|
+
- Present balanced viewpoints
|
658
|
+
- Highlight key findings
|
659
|
+
PROMPT
|
660
|
+
}
|
661
|
+
}]
|
662
|
+
end
|
663
|
+
|
664
|
+
def coder_messages(context)
|
665
|
+
lang = context["language"] || "multiple languages"
|
666
|
+
[{
|
667
|
+
role: "system",
|
668
|
+
content: {
|
669
|
+
type: "text",
|
670
|
+
text: <<~PROMPT
|
671
|
+
As a coder working with #{lang}:
|
672
|
+
- Write clean, efficient code
|
673
|
+
- Follow best practices and conventions
|
674
|
+
- Include error handling
|
675
|
+
- Add helpful comments
|
676
|
+
PROMPT
|
677
|
+
}
|
678
|
+
}]
|
679
|
+
end
|
680
|
+
end
|
681
|
+
```
|
682
|
+
|
683
|
+
## Testing Prompts
|
684
|
+
|
685
|
+
```ruby
|
686
|
+
require 'minitest/autorun'
|
687
|
+
require 'tsikol/test_helpers'
|
688
|
+
|
689
|
+
class PromptTest < Minitest::Test
|
690
|
+
include Tsikol::TestHelpers::Assertions
|
691
|
+
|
692
|
+
def setup
|
693
|
+
@server = Tsikol::Server.new(name: "test")
|
694
|
+
@server.register_prompt_instance(CodeReviewer.new)
|
695
|
+
@client = Tsikol::TestHelpers::TestClient.new(@server)
|
696
|
+
@client.initialize_connection
|
697
|
+
end
|
698
|
+
|
699
|
+
def test_basic_prompt
|
700
|
+
response = @client.get_prompt("code_reviewer", {
|
701
|
+
"code" => "def hello\n puts 'Hello'\nend",
|
702
|
+
"language" => "ruby"
|
703
|
+
})
|
704
|
+
|
705
|
+
assert_successful_response(response)
|
706
|
+
|
707
|
+
messages = response.dig(:result, :messages)
|
708
|
+
assert messages.is_a?(Array)
|
709
|
+
assert messages.length >= 2
|
710
|
+
|
711
|
+
# Verify system message
|
712
|
+
system_msg = messages.find { |m| m[:role] == "system" }
|
713
|
+
assert system_msg
|
714
|
+
assert_match /code review/i, system_msg[:content][:text]
|
715
|
+
|
716
|
+
# Verify user message contains code
|
717
|
+
user_msg = messages.find { |m| m[:role] == "user" }
|
718
|
+
assert user_msg
|
719
|
+
assert_match /def hello/, user_msg[:content][:text]
|
720
|
+
end
|
721
|
+
|
722
|
+
def test_argument_validation
|
723
|
+
# Missing required argument
|
724
|
+
response = @client.get_prompt("code_reviewer", {
|
725
|
+
"language" => "ruby"
|
726
|
+
# Missing "code" argument
|
727
|
+
})
|
728
|
+
|
729
|
+
assert_error_response(response, -32602)
|
730
|
+
assert_match /code.*required/i, response[:error][:message]
|
731
|
+
end
|
732
|
+
|
733
|
+
def test_enum_validation
|
734
|
+
response = @client.get_prompt("code_reviewer", {
|
735
|
+
"code" => "print('Hello')",
|
736
|
+
"language" => "cobol" # Not in enum
|
737
|
+
})
|
738
|
+
|
739
|
+
assert_error_response(response, -32602)
|
740
|
+
assert_match /must be one of/i, response[:error][:message]
|
741
|
+
end
|
742
|
+
|
743
|
+
def test_optional_arguments_with_defaults
|
744
|
+
response = @client.get_prompt("code_reviewer", {
|
745
|
+
"code" => "console.log('Hello');",
|
746
|
+
"language" => "javascript"
|
747
|
+
# "style" will use default value
|
748
|
+
})
|
749
|
+
|
750
|
+
assert_successful_response(response)
|
751
|
+
|
752
|
+
# Should include default style in prompt
|
753
|
+
messages = response.dig(:result, :messages)
|
754
|
+
text = messages.map { |m| m[:content][:text] }.join(" ")
|
755
|
+
assert_match /standard/i, text # Default style
|
756
|
+
end
|
757
|
+
|
758
|
+
def test_completion
|
759
|
+
response = @client.complete(
|
760
|
+
{ type: "ref/prompt", name: "code_reviewer" },
|
761
|
+
{ name: "language", value: "ja" }
|
762
|
+
)
|
763
|
+
|
764
|
+
assert_successful_response(response)
|
765
|
+
values = response.dig(:result, :completion, :values)
|
766
|
+
assert values.include?("javascript")
|
767
|
+
assert values.include?("java")
|
768
|
+
end
|
769
|
+
end
|
770
|
+
```
|
771
|
+
|
772
|
+
## Best Practices
|
773
|
+
|
774
|
+
### Clear System Prompts
|
775
|
+
|
776
|
+
```ruby
|
777
|
+
def get_messages(task:)
|
778
|
+
[
|
779
|
+
{
|
780
|
+
role: "system",
|
781
|
+
content: {
|
782
|
+
type: "text",
|
783
|
+
text: <<~PROMPT
|
784
|
+
You are an AI assistant specialized in #{specialty}.
|
785
|
+
|
786
|
+
Your responsibilities:
|
787
|
+
1. Provide accurate information
|
788
|
+
2. Follow best practices
|
789
|
+
3. Explain your reasoning
|
790
|
+
|
791
|
+
Constraints:
|
792
|
+
- Stay within your area of expertise
|
793
|
+
- Acknowledge when you're unsure
|
794
|
+
- Provide sources when possible
|
795
|
+
PROMPT
|
796
|
+
}
|
797
|
+
},
|
798
|
+
{
|
799
|
+
role: "user",
|
800
|
+
content: { type: "text", text: task }
|
801
|
+
}
|
802
|
+
]
|
803
|
+
end
|
804
|
+
```
|
805
|
+
|
806
|
+
### Dynamic Context
|
807
|
+
|
808
|
+
```ruby
|
809
|
+
def get_messages(query:, include_context: true)
|
810
|
+
messages = [base_system_message]
|
811
|
+
|
812
|
+
if include_context
|
813
|
+
# Add relevant context
|
814
|
+
messages << context_message(query)
|
815
|
+
end
|
816
|
+
|
817
|
+
# Add examples if helpful
|
818
|
+
if would_benefit_from_examples?(query)
|
819
|
+
messages.concat(example_messages)
|
820
|
+
end
|
821
|
+
|
822
|
+
messages << user_message(query)
|
823
|
+
messages
|
824
|
+
end
|
825
|
+
```
|
826
|
+
|
827
|
+
### Error Handling
|
828
|
+
|
829
|
+
```ruby
|
830
|
+
def get_messages(**args)
|
831
|
+
validate_arguments!(args)
|
832
|
+
|
833
|
+
messages = build_messages(args)
|
834
|
+
|
835
|
+
# Ensure valid message format
|
836
|
+
validate_messages!(messages)
|
837
|
+
|
838
|
+
messages
|
839
|
+
rescue => e
|
840
|
+
# Return minimal valid prompt on error
|
841
|
+
[
|
842
|
+
{
|
843
|
+
role: "user",
|
844
|
+
content: {
|
845
|
+
type: "text",
|
846
|
+
text: "Error building prompt: #{e.message}"
|
847
|
+
}
|
848
|
+
}
|
849
|
+
]
|
850
|
+
end
|
851
|
+
```
|
852
|
+
|
853
|
+
## See Also
|
854
|
+
|
855
|
+
- [Prompts Guide](../guides/prompts.md)
|
856
|
+
- [Server API](server.md)
|
857
|
+
- [Testing Guide](../guides/testing.md)
|
858
|
+
- [Sampling Guide](../guides/sampling.md)
|