spectre_ai 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +69 -0
- data/README.md +113 -14
- data/lib/generators/spectre/templates/spectre_initializer.rb +2 -0
- data/lib/spectre/openai/completions.rb +103 -50
- data/lib/spectre/prompt.rb +63 -3
- data/lib/spectre/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5cf3f5f31178a8456ccbb32bf9d68560efa5873751d3b3ca95c71921734b5da0
|
4
|
+
data.tar.gz: 9e96ac4a9eb3bb69dca886cff6570450f6380100c6df2df789f1c0bc0b7327f6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 23dc62f85aa5d69faa16036349560dcf0a97f214f636598387921ba4fa63b48863a69db2e60518d05aeae383a1b27ba8a82ff03bf40ec9fa46b5c7fa059ce546
|
7
|
+
data.tar.gz: 12203cbdfc16fb9e4983362eee4a700f33ba220516ad75663f0bfafc57a2b503c164fb7e7efc90913f3cbb73c6f62d589c9a39b4fdeda0e22fa1decbfc14732c
|
data/CHANGELOG.md
CHANGED
@@ -0,0 +1,69 @@
|
|
1
|
+
# Changelog for Version 1.0.1
|
2
|
+
|
3
|
+
**Release Date:** [19th Sep 2024]
|
4
|
+
|
5
|
+
**Improvements to Prompt Class**
|
6
|
+
|
7
|
+
1. Escaping Special Characters in YAML/ERB:
|
8
|
+
|
9
|
+
• Implemented automatic escaping of special characters in YAML/ERB templates, such as &, <, >, ", and ' to prevent parsing issues.
|
10
|
+
|
11
|
+
• Special characters are escaped before rendering the YAML file to ensure compatibility with YAML parsers.
|
12
|
+
|
13
|
+
• After rendering, the escaped characters are converted back to their original form, ensuring the final output is clean and readable.
|
14
|
+
|
15
|
+
• This update prevents errors that occur when rendering templates with complex queries or strings containing special characters.
|
16
|
+
|
17
|
+
Example:
|
18
|
+
|
19
|
+
```yaml
|
20
|
+
user: |
|
21
|
+
User's query: <%= @query %>
|
22
|
+
Context: <%= @responses.join(", ") %>
|
23
|
+
```
|
24
|
+
|
25
|
+
Before this change, queries or responses containing special characters might have caused YAML parsing errors. This update ensures that even complex strings are handled safely and returned in their original form.
|
26
|
+
|
27
|
+
To upgrade, update your Gemfile to version 1.0.1 and run bundle install. Make sure your YAML/ERB templates do not manually escape special characters anymore, as the Prompt class will handle it automatically.
|
28
|
+
|
29
|
+
# Changelog for Version 1.1.0
|
30
|
+
|
31
|
+
**Release Date:** [7th Oct 2024]
|
32
|
+
|
33
|
+
**New Features:**
|
34
|
+
|
35
|
+
* **Tool _(Function Calling)_ Integration:** Added support for tools parameter to enable function calling during completions. Now you can specify an array of tool definitions that the model can use to call specific functions.
|
36
|
+
|
37
|
+
* **Enhanced Message Handling:** Replaced individual prompt parameters (user_prompt, system_prompt, assistant_prompt) with a single messages array parameter, which accepts a sequence of messages with their roles and contents. This provides more flexibility in managing conversations.
|
38
|
+
|
39
|
+
* **Response Validation:** Introduced a handle_response method to handle different finish_reason cases more effectively, including content filtering and tool call handling.
|
40
|
+
|
41
|
+
* **Improved Error Handling:**
|
42
|
+
Added more specific error messages for cases like refusal (Refusal), incomplete response due to token limits (Incomplete response), and content filtering (Content filtered).
|
43
|
+
Enhanced JSON parsing error handling with more descriptive messages.
|
44
|
+
|
45
|
+
* **Request Validation:** Implemented message validation to ensure the messages parameter is not empty and follows the required format. Raises an error if validation fails.
|
46
|
+
|
47
|
+
* **Support for Structured Output:** Integrated support for json_schema parameter in the request body to enforce structured output responses.
|
48
|
+
|
49
|
+
* **Skip Request on Empty Messages:** The class will now skip sending a request if the messages parameter is empty or invalid, reducing unnecessary API calls.
|
50
|
+
|
51
|
+
**Breaking Changes:**
|
52
|
+
|
53
|
+
**Message Parameter Refactor**: The previous individual prompt parameters (user_prompt, system_prompt, assistant_prompt) have been consolidated into a single messages array. This may require updating any existing code using the old parameters.
|
54
|
+
|
55
|
+
**Bug Fixes:**
|
56
|
+
|
57
|
+
* **API Key Check:** Improved error handling for cases when the API key is not configured, providing a more specific exception.
|
58
|
+
|
59
|
+
* **Error Messages:** Enhanced error messages for various edge cases, including content filtering and incomplete responses due to token limits.
|
60
|
+
|
61
|
+
**Refinements:**
|
62
|
+
|
63
|
+
Code Refactoring:
|
64
|
+
* Moved message validation into a dedicated validate_messages! method for clarity and reusability.
|
65
|
+
* Simplified the generate_body method to include the tools and json_schema parameters more effectively.
|
66
|
+
|
67
|
+
**Documentation:** Updated class-level documentation and method comments for better clarity and understanding of the class’s functionality and usage.
|
68
|
+
|
69
|
+
This version enhances the flexibility and robustness of the Completions class, enabling more complex interactions and better error handling for different types of API responses.
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Spectre
|
1
|
+
# Spectre [![Gem Version](https://badge.fury.io/rb/spectre_ai.svg)](https://badge.fury.io/rb/spectre_ai)
|
2
2
|
|
3
3
|
**Spectre** is a Ruby gem that makes it easy to AI-enable your Ruby on Rails application. Currently, Spectre focuses on helping developers create embeddings, perform vector-based searches, create chat completions, and manage dynamic prompts — ideal for applications that are featuring RAG (Retrieval-Augmented Generation), chatbots and dynamic prompts.
|
4
4
|
|
@@ -18,7 +18,7 @@
|
|
18
18
|
Add this line to your application's Gemfile:
|
19
19
|
|
20
20
|
```ruby
|
21
|
-
gem '
|
21
|
+
gem 'spectre_ai'
|
22
22
|
```
|
23
23
|
|
24
24
|
And then execute:
|
@@ -30,7 +30,7 @@ bundle install
|
|
30
30
|
Or install it yourself as:
|
31
31
|
|
32
32
|
```bash
|
33
|
-
gem install
|
33
|
+
gem install spectre_ai
|
34
34
|
```
|
35
35
|
|
36
36
|
## Usage
|
@@ -175,25 +175,36 @@ Spectre provides an interface to create chat completions using your configured L
|
|
175
175
|
To create a simple chat completion, use the `Spectre.provider_module::Completions.create` method. You can provide a user prompt and an optional system prompt to guide the response:
|
176
176
|
|
177
177
|
```ruby
|
178
|
+
messages = [
|
179
|
+
{ role: 'system', content: "You are a funny assistant." },
|
180
|
+
{ role: 'user', content: "Tell me a joke." }
|
181
|
+
]
|
182
|
+
|
178
183
|
Spectre.provider_module::Completions.create(
|
179
|
-
|
180
|
-
system_prompt: "You are a funny assistant."
|
184
|
+
messages: messages
|
181
185
|
)
|
186
|
+
|
182
187
|
```
|
183
188
|
|
184
189
|
This sends the request to the LLM provider’s API and returns the chat completion.
|
185
190
|
|
186
191
|
**Customizing the Completion**
|
187
192
|
|
188
|
-
You can customize the behavior by specifying additional parameters such as the model
|
193
|
+
You can customize the behavior by specifying additional parameters such as the model, maximum number of tokens, and any tools needed for function calls:
|
189
194
|
|
190
195
|
```ruby
|
196
|
+
messages = [
|
197
|
+
{ role: 'system', content: "You are a funny assistant." },
|
198
|
+
{ role: 'user', content: "Tell me a joke." },
|
199
|
+
{ role: 'assistant', content: "Sure, here's a joke!" }
|
200
|
+
]
|
201
|
+
|
191
202
|
Spectre.provider_module::Completions.create(
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
model: "gpt-4"
|
203
|
+
messages: messages,
|
204
|
+
model: "gpt-4",
|
205
|
+
max_tokens: 50
|
196
206
|
)
|
207
|
+
|
197
208
|
```
|
198
209
|
|
199
210
|
**Using a JSON Schema for Structured Output**
|
@@ -214,15 +225,100 @@ json_schema = {
|
|
214
225
|
}
|
215
226
|
}
|
216
227
|
|
228
|
+
messages = [
|
229
|
+
{ role: 'system', content: "You are a knowledgeable assistant." },
|
230
|
+
{ role: 'user', content: "What is the capital of France?" }
|
231
|
+
]
|
232
|
+
|
217
233
|
Spectre.provider_module::Completions.create(
|
218
|
-
|
219
|
-
system_prompt: "You are a knowledgeable assistant.",
|
234
|
+
messages: messages,
|
220
235
|
json_schema: json_schema
|
221
236
|
)
|
237
|
+
|
222
238
|
```
|
223
239
|
|
224
240
|
This structured format guarantees that the response adheres to the schema you’ve provided, ensuring more predictable and controlled results.
|
225
241
|
|
242
|
+
**Using Tools for Function Calling**
|
243
|
+
|
244
|
+
You can incorporate tools (function calls) in your completion to handle more complex interactions such as fetching external information via API or performing calculations. Define tools using the function call format and include them in the request:
|
245
|
+
|
246
|
+
```ruby
|
247
|
+
tools = [
|
248
|
+
{
|
249
|
+
type: "function",
|
250
|
+
function: {
|
251
|
+
name: "get_delivery_date",
|
252
|
+
description: "Get the delivery date for a customer's order.",
|
253
|
+
parameters: {
|
254
|
+
type: "object",
|
255
|
+
properties: {
|
256
|
+
order_id: { type: "string", description: "The customer's order ID." }
|
257
|
+
},
|
258
|
+
required: ["order_id"],
|
259
|
+
additionalProperties: false
|
260
|
+
}
|
261
|
+
}
|
262
|
+
}
|
263
|
+
]
|
264
|
+
|
265
|
+
messages = [
|
266
|
+
{ role: 'system', content: "You are a helpful customer support assistant." },
|
267
|
+
{ role: 'user', content: "Can you tell me the delivery date for my order?" }
|
268
|
+
]
|
269
|
+
|
270
|
+
Spectre.provider_module::Completions.create(
|
271
|
+
messages: messages,
|
272
|
+
tools: tools
|
273
|
+
)
|
274
|
+
```
|
275
|
+
|
276
|
+
This setup allows the model to call specific tools (or functions) based on the user's input. The model can then generate a tool call to get necessary information and integrate it into the conversation.
|
277
|
+
|
278
|
+
**Handling Responses from Completions with Tools**
|
279
|
+
|
280
|
+
When tools (function calls) are included in a completion request, the response might include `tool_calls` with relevant details for executing the function.
|
281
|
+
|
282
|
+
Here’s an example of how the response might look when a tool call is made:
|
283
|
+
|
284
|
+
```ruby
|
285
|
+
response = Spectre.provider_module::Completions.create(
|
286
|
+
messages: messages,
|
287
|
+
tools: tools
|
288
|
+
)
|
289
|
+
|
290
|
+
# Sample response structure when a tool call is triggered:
|
291
|
+
# {
|
292
|
+
# :tool_calls=>[{
|
293
|
+
# "id" => "call_gqvSz1JTDfUyky7ghqY1wMoy",
|
294
|
+
# "type" => "function",
|
295
|
+
# "function" => {
|
296
|
+
# "name" => "get_lead_count",
|
297
|
+
# "arguments" => "{\"account_id\":\"acc_12312\"}"
|
298
|
+
# }
|
299
|
+
# }],
|
300
|
+
# :content => nil
|
301
|
+
# }
|
302
|
+
|
303
|
+
if response[:tool_calls]
|
304
|
+
tool_call = response[:tool_calls].first
|
305
|
+
|
306
|
+
# You can now perform the function using the provided data
|
307
|
+
# For example, get the lead count by account_id
|
308
|
+
account_id = JSON.parse(tool_call['function']['arguments'])['account_id']
|
309
|
+
lead_count = get_lead_count(account_id) # Assuming you have a method for this
|
310
|
+
|
311
|
+
# Respond back with the function result
|
312
|
+
completion_response = Spectre.provider_module::Completions.create(
|
313
|
+
messages: [
|
314
|
+
{ role: 'assistant', content: "There are #{lead_count} leads for account #{account_id}." }
|
315
|
+
]
|
316
|
+
)
|
317
|
+
else
|
318
|
+
puts "Model response: #{response[:content]}"
|
319
|
+
end
|
320
|
+
```
|
321
|
+
|
226
322
|
### 6. Creating Dynamic Prompts
|
227
323
|
|
228
324
|
Spectre provides a system for creating dynamic prompts based on templates. You can define reusable prompt templates and render them with different parameters in your Rails app (think Ruby on Rails view partials).
|
@@ -287,9 +383,12 @@ You can also combine completions and prompts like so:
|
|
287
383
|
|
288
384
|
```ruby
|
289
385
|
Spectre.provider_module::Completions.create(
|
290
|
-
|
291
|
-
|
386
|
+
messages: [
|
387
|
+
{ role: 'system', content: Spectre::Prompt.render(template: 'rag/system') },
|
388
|
+
{ role: 'user', content: Spectre::Prompt.render(template: 'rag/user', locals: { query: @query, user: @user }) }
|
389
|
+
]
|
292
390
|
)
|
391
|
+
|
293
392
|
```
|
294
393
|
|
295
394
|
## Contributing
|
@@ -10,21 +10,22 @@ module Spectre
|
|
10
10
|
API_URL = 'https://api.openai.com/v1/chat/completions'
|
11
11
|
DEFAULT_MODEL = 'gpt-4o-mini'
|
12
12
|
|
13
|
-
# Class method to generate a completion based on
|
13
|
+
# Class method to generate a completion based on user messages and optional tools
|
14
14
|
#
|
15
|
-
# @param
|
16
|
-
# @param
|
17
|
-
# @param
|
18
|
-
# @param
|
19
|
-
# @param
|
20
|
-
# @
|
21
|
-
# @
|
22
|
-
# @raise [
|
23
|
-
|
24
|
-
def self.create(user_prompt:, system_prompt: "You are a helpful assistant.", assistant_prompt: nil, model: DEFAULT_MODEL, json_schema: nil, max_tokens: nil)
|
15
|
+
# @param messages [Array<Hash>] The conversation messages, each with a role and content
|
16
|
+
# @param model [String] The model to be used for generating completions, defaults to DEFAULT_MODEL
|
17
|
+
# @param json_schema [Hash, nil] An optional JSON schema to enforce structured output
|
18
|
+
# @param max_tokens [Integer] The maximum number of tokens for the completion (default: 50)
|
19
|
+
# @param tools [Array<Hash>, nil] An optional array of tool definitions for function calling
|
20
|
+
# @return [Hash] The parsed response including any function calls or content
|
21
|
+
# @raise [APIKeyNotConfiguredError] If the API key is not set
|
22
|
+
# @raise [RuntimeError] For general API errors or unexpected issues
|
23
|
+
def self.create(messages:, model: DEFAULT_MODEL, json_schema: nil, max_tokens: nil, tools: nil)
|
25
24
|
api_key = Spectre.api_key
|
26
25
|
raise APIKeyNotConfiguredError, "API key is not configured" unless api_key
|
27
26
|
|
27
|
+
validate_messages!(messages)
|
28
|
+
|
28
29
|
uri = URI(API_URL)
|
29
30
|
http = Net::HTTP.new(uri.host, uri.port)
|
30
31
|
http.use_ssl = true
|
@@ -36,7 +37,7 @@ module Spectre
|
|
36
37
|
'Authorization' => "Bearer #{api_key}"
|
37
38
|
})
|
38
39
|
|
39
|
-
request.body = generate_body(
|
40
|
+
request.body = generate_body(messages, model, json_schema, max_tokens, tools).to_json
|
40
41
|
response = http.request(request)
|
41
42
|
|
42
43
|
unless response.is_a?(Net::HTTPSuccess)
|
@@ -45,18 +46,7 @@ module Spectre
|
|
45
46
|
|
46
47
|
parsed_response = JSON.parse(response.body)
|
47
48
|
|
48
|
-
|
49
|
-
if parsed_response.dig('choices', 0, 'message', 'refusal')
|
50
|
-
raise "Refusal: #{parsed_response.dig('choices', 0, 'message', 'refusal')}"
|
51
|
-
end
|
52
|
-
|
53
|
-
# Check if the finish reason is "length", indicating incomplete response
|
54
|
-
if parsed_response.dig('choices', 0, 'finish_reason') == "length"
|
55
|
-
raise "Incomplete response: The completion was cut off due to token limit."
|
56
|
-
end
|
57
|
-
|
58
|
-
# Return the structured output if it's included
|
59
|
-
parsed_response.dig('choices', 0, 'message', 'content')
|
49
|
+
handle_response(parsed_response)
|
60
50
|
rescue JSON::ParserError => e
|
61
51
|
raise "JSON Parse Error: #{e.message}"
|
62
52
|
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
@@ -65,40 +55,103 @@ module Spectre
|
|
65
55
|
|
66
56
|
private
|
67
57
|
|
68
|
-
#
|
58
|
+
# Validate the structure and content of the messages array.
|
59
|
+
#
|
60
|
+
# @param messages [Array<Hash>] The array of message hashes to validate.
|
69
61
|
#
|
70
|
-
# @
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
# Add the assistant prompt if provided
|
84
|
-
messages << { role: 'assistant', content: assistant_prompt } if assistant_prompt
|
62
|
+
# @raise [ArgumentError] if the messages array is not in the expected format or contains invalid data.
|
63
|
+
def self.validate_messages!(messages)
|
64
|
+
# Check if messages is an array of hashes.
|
65
|
+
# This ensures that the input is in the correct format for message processing.
|
66
|
+
unless messages.is_a?(Array) && messages.all? { |msg| msg.is_a?(Hash) }
|
67
|
+
raise ArgumentError, "Messages must be an array of message hashes."
|
68
|
+
end
|
69
|
+
|
70
|
+
# Check if the array is empty.
|
71
|
+
# This prevents requests with no messages, which would be invalid.
|
72
|
+
if messages.empty?
|
73
|
+
raise ArgumentError, "Messages cannot be empty."
|
74
|
+
end
|
85
75
|
|
76
|
+
# Iterate through each message and perform detailed validation.
|
77
|
+
messages.each_with_index do |msg, index|
|
78
|
+
# Check if each message hash contains the required keys: :role and :content.
|
79
|
+
# These keys are necessary for defining the type of message and its content.
|
80
|
+
unless msg.key?(:role) && msg.key?(:content)
|
81
|
+
raise ArgumentError, "Message at index #{index} must contain both :role and :content keys."
|
82
|
+
end
|
83
|
+
|
84
|
+
# Check if the role is one of the allowed values: 'system', 'user', or 'assistant'.
|
85
|
+
# This ensures that each message has a valid role identifier.
|
86
|
+
unless %w[system user assistant].include?(msg[:role])
|
87
|
+
raise ArgumentError, "Invalid role '#{msg[:role]}' at index #{index}. Valid roles are 'system', 'user', 'assistant'."
|
88
|
+
end
|
89
|
+
|
90
|
+
# Check if the content is a non-empty string.
|
91
|
+
# This prevents empty or non-string content, which would be meaningless in a conversation.
|
92
|
+
unless msg[:content].is_a?(String) && !msg[:content].strip.empty?
|
93
|
+
raise ArgumentError, "Content for message at index #{index} must be a non-empty string."
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Helper method to generate the request body
|
99
|
+
#
|
100
|
+
# @param messages [Array<Hash>] The conversation messages, each with a role and content
|
101
|
+
# @param model [String] The model to be used for generating completions
|
102
|
+
# @param json_schema [Hash, nil] An optional JSON schema to enforce structured output
|
103
|
+
# @param max_tokens [Integer, nil] The maximum number of tokens for the completion
|
104
|
+
# @param tools [Array<Hash>, nil] An optional array of tool definitions for function calling
|
105
|
+
# @return [Hash] The body for the API request
|
106
|
+
def self.generate_body(messages, model, json_schema, max_tokens, tools)
|
86
107
|
body = {
|
87
108
|
model: model,
|
88
|
-
messages: messages
|
109
|
+
messages: messages
|
89
110
|
}
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
if
|
94
|
-
body[:response_format] = {
|
95
|
-
type: 'json_schema',
|
96
|
-
json_schema: json_schema
|
97
|
-
}
|
98
|
-
end
|
111
|
+
|
112
|
+
body[:max_tokens] = max_tokens if max_tokens
|
113
|
+
body[:response_format] = { type: 'json_schema', json_schema: json_schema } if json_schema
|
114
|
+
body[:tools] = tools if tools # Add the tools to the request body if provided
|
99
115
|
|
100
116
|
body
|
101
117
|
end
|
118
|
+
|
119
|
+
# Handles the API response, raising errors for specific cases and returning structured content otherwise
|
120
|
+
#
|
121
|
+
# @param response [Hash] The parsed API response
|
122
|
+
# @return [Hash] The relevant data based on the finish reason
|
123
|
+
def self.handle_response(response)
|
124
|
+
message = response.dig('choices', 0, 'message')
|
125
|
+
finish_reason = response.dig('choices', 0, 'finish_reason')
|
126
|
+
|
127
|
+
# Check if the response contains a refusal
|
128
|
+
if message['refusal']
|
129
|
+
raise "Refusal: #{message['refusal']}"
|
130
|
+
end
|
131
|
+
|
132
|
+
# Check if the finish reason is "length", indicating incomplete response
|
133
|
+
if finish_reason == "length"
|
134
|
+
raise "Incomplete response: The completion was cut off due to token limit."
|
135
|
+
end
|
136
|
+
|
137
|
+
# Check if the finish reason is "content_filter", indicating policy violations
|
138
|
+
if finish_reason == "content_filter"
|
139
|
+
raise "Content filtered: The model's output was blocked due to policy violations."
|
140
|
+
end
|
141
|
+
|
142
|
+
# Check if the model made a function call
|
143
|
+
if finish_reason == "function_call" || finish_reason == "tool_calls"
|
144
|
+
return { tool_calls: message['tool_calls'], content: message['content'] }
|
145
|
+
end
|
146
|
+
|
147
|
+
# If the response finished normally, return the content
|
148
|
+
if finish_reason == "stop"
|
149
|
+
return { content: message['content'] }
|
150
|
+
end
|
151
|
+
|
152
|
+
# Handle unexpected finish reasons
|
153
|
+
raise "Unexpected finish_reason: #{finish_reason}"
|
154
|
+
end
|
102
155
|
end
|
103
156
|
end
|
104
157
|
end
|
data/lib/spectre/prompt.rb
CHANGED
@@ -19,13 +19,26 @@ module Spectre
|
|
19
19
|
|
20
20
|
raise "Prompt file not found: #{file_path}" unless File.exist?(file_path)
|
21
21
|
|
22
|
+
# Preprocess the locals before rendering the YAML file
|
23
|
+
preprocessed_locals = preprocess_locals(locals)
|
24
|
+
|
22
25
|
template_content = File.read(file_path)
|
23
26
|
erb_template = ERB.new(template_content)
|
24
27
|
|
25
|
-
context = Context.new(
|
28
|
+
context = Context.new(preprocessed_locals)
|
26
29
|
rendered_prompt = erb_template.result(context.get_binding)
|
27
30
|
|
28
|
-
YAML.safe_load
|
31
|
+
# YAML.safe_load returns a hash, so fetch the correct part based on the prompt
|
32
|
+
parsed_yaml = YAML.safe_load(rendered_prompt)[prompt]
|
33
|
+
|
34
|
+
# Convert special characters back after YAML processing
|
35
|
+
convert_special_chars_back(parsed_yaml)
|
36
|
+
rescue Errno::ENOENT
|
37
|
+
raise "Template file not found at path: #{file_path}"
|
38
|
+
rescue Psych::SyntaxError => e
|
39
|
+
raise "YAML Syntax Error in file #{file_path}: #{e.message}"
|
40
|
+
rescue StandardError => e
|
41
|
+
raise "Error rendering prompt for template '#{template}': #{e.message}"
|
29
42
|
end
|
30
43
|
|
31
44
|
private
|
@@ -48,7 +61,54 @@ module Spectre
|
|
48
61
|
"#{PROMPTS_PATH}/#{type}/#{prompt}.yml.erb"
|
49
62
|
end
|
50
63
|
|
51
|
-
#
|
64
|
+
# Preprocess locals recursively to escape special characters in strings
|
65
|
+
#
|
66
|
+
# @param value [Object] The value to process (string, array, hash, etc.)
|
67
|
+
# @return [Object] Processed value with special characters escaped
|
68
|
+
def self.preprocess_locals(value)
|
69
|
+
case value
|
70
|
+
when String
|
71
|
+
escape_special_chars(value)
|
72
|
+
when Hash
|
73
|
+
value.transform_values { |v| preprocess_locals(v) } # Recurse into hash values
|
74
|
+
when Array
|
75
|
+
value.map { |item| preprocess_locals(item) } # Recurse into array items
|
76
|
+
else
|
77
|
+
value
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Escape special characters in strings to avoid YAML parsing issues
|
82
|
+
#
|
83
|
+
# @param value [String] The string to process
|
84
|
+
# @return [String] The processed string with special characters escaped
|
85
|
+
def self.escape_special_chars(value)
|
86
|
+
value.gsub('&', '&')
|
87
|
+
.gsub('<', '<')
|
88
|
+
.gsub('>', '>')
|
89
|
+
.gsub('"', '"')
|
90
|
+
.gsub("'", ''')
|
91
|
+
.gsub("\n", '\\n')
|
92
|
+
.gsub("\r", '\\r')
|
93
|
+
.gsub("\t", '\\t')
|
94
|
+
end
|
95
|
+
|
96
|
+
# Convert special characters back to their original form after YAML processing
|
97
|
+
#
|
98
|
+
# @param value [String] The string to process
|
99
|
+
# @return [String] The processed string with original special characters restored
|
100
|
+
def self.convert_special_chars_back(value)
|
101
|
+
value.gsub('&', '&')
|
102
|
+
.gsub('<', '<')
|
103
|
+
.gsub('>', '>')
|
104
|
+
.gsub('"', '"')
|
105
|
+
.gsub(''', "'")
|
106
|
+
.gsub('\\n', "\n")
|
107
|
+
.gsub('\\r', "\r")
|
108
|
+
.gsub('\\t', "\t")
|
109
|
+
end
|
110
|
+
|
111
|
+
# Helper class to handle the binding for ERB template rendering
|
52
112
|
class Context
|
53
113
|
def initialize(locals)
|
54
114
|
locals.each do |key, value|
|
data/lib/spectre/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spectre_ai
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ilya Klapatok
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2024-
|
12
|
+
date: 2024-10-07 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec-rails
|