ruby_todo 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +22 -1
- data/CHANGELOG.md +5 -0
- data/archived_tasks_export_20250331.json +22 -0
- data/in_progress_tasks_export_20250331.json +33 -0
- data/lib/ruby_todo/ai_assistant/openai_integration.rb +108 -9
- data/lib/ruby_todo/ai_assistant/param_extractor.rb +6 -2
- data/lib/ruby_todo/ai_assistant/task_creator.rb +5 -0
- data/lib/ruby_todo/ai_assistant/task_response_processor.rb +63 -0
- data/lib/ruby_todo/cli.rb +1 -0
- data/lib/ruby_todo/commands/ai_assistant.rb +922 -62
- data/lib/ruby_todo/commands/notebook_commands.rb +8 -2
- data/lib/ruby_todo/database.rb +16 -0
- data/lib/ruby_todo/version.rb +1 -1
- data/pr_template.md +58 -0
- data/sorted_tests.txt +59 -0
- data/test_methods.txt +59 -0
- data/todo_tasks_export_20250331.json +55 -0
- metadata +8 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 674c8336357f505a1e280c70b25f6e2202de7535da7f51a66a8fc511de855656
|
4
|
+
data.tar.gz: 2772f818887aa4a20d4490f46c2d619720f27a135c3166f98c8412f7f216e968
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 282597c10ca5471923d5f2f617fe56c10b7daec5e8a03e7440a6266e5d4b1b4bec4a3ae5bb78fecd696041d76c89c4cfc29b36595cbd53b27e5278947232c189
|
7
|
+
data.tar.gz: 0ee9b319591eb7e00e9a84cd029882e1850ee5ac800e547e17418be488d60ece0b7282785d888c372c2c284043b92b68f89e8de99a19a968da2894fd3d236544
|
data/.rubocop.yml
CHANGED
@@ -13,6 +13,8 @@ plugins:
|
|
13
13
|
# Increase line length limit
|
14
14
|
Layout/LineLength:
|
15
15
|
Max: 120
|
16
|
+
Exclude:
|
17
|
+
- 'test/integration/**/*'
|
16
18
|
|
17
19
|
# Relaxed metrics for all files
|
18
20
|
Metrics/AbcSize:
|
@@ -22,9 +24,11 @@ Metrics/AbcSize:
|
|
22
24
|
- 'lib/ruby_todo/cli.rb'
|
23
25
|
- 'lib/ruby_todo/database.rb'
|
24
26
|
- 'lib/ruby_todo/models/template.rb'
|
27
|
+
- 'lib/ruby_todo/commands/ai_assistant.rb'
|
28
|
+
- 'lib/ruby_todo/ai_assistant/**/*'
|
25
29
|
|
26
30
|
Metrics/ClassLength:
|
27
|
-
Max:
|
31
|
+
Max: 900
|
28
32
|
Exclude:
|
29
33
|
- 'test/**/*'
|
30
34
|
|
@@ -34,12 +38,27 @@ Metrics/MethodLength:
|
|
34
38
|
- 'test/**/*'
|
35
39
|
- 'lib/ruby_todo/cli.rb'
|
36
40
|
- 'lib/ruby_todo/database.rb'
|
41
|
+
- 'lib/ruby_todo/commands/ai_assistant.rb'
|
42
|
+
- 'lib/ruby_todo/ai_assistant/**/*'
|
43
|
+
|
44
|
+
Metrics/ModuleLength:
|
45
|
+
Max: 200
|
46
|
+
Exclude:
|
47
|
+
- 'test/**/*'
|
48
|
+
- 'lib/ruby_todo/commands/ai_assistant.rb'
|
49
|
+
- 'lib/ruby_todo/ai_assistant/**/*'
|
37
50
|
|
38
51
|
Metrics/CyclomaticComplexity:
|
39
52
|
Max: 15
|
53
|
+
Exclude:
|
54
|
+
- 'lib/ruby_todo/commands/ai_assistant.rb'
|
55
|
+
- 'lib/ruby_todo/ai_assistant/**/*'
|
40
56
|
|
41
57
|
Metrics/PerceivedComplexity:
|
42
58
|
Max: 20
|
59
|
+
Exclude:
|
60
|
+
- 'lib/ruby_todo/commands/ai_assistant.rb'
|
61
|
+
- 'lib/ruby_todo/ai_assistant/**/*'
|
43
62
|
|
44
63
|
# Allow longer blocks in test files and CLI
|
45
64
|
Metrics/BlockLength:
|
@@ -47,6 +66,8 @@ Metrics/BlockLength:
|
|
47
66
|
Exclude:
|
48
67
|
- 'test/**/*'
|
49
68
|
- 'ruby_todo.gemspec'
|
69
|
+
- 'lib/ruby_todo/commands/ai_assistant.rb'
|
70
|
+
- 'lib/ruby_todo/ai_assistant/**/*'
|
50
71
|
|
51
72
|
# Relaxed rules for test descriptions
|
52
73
|
Naming/VariableNumber:
|
data/CHANGELOG.md
CHANGED
@@ -0,0 +1,22 @@
|
|
1
|
+
{
|
2
|
+
"notebooks": [
|
3
|
+
{
|
4
|
+
"name": "test_notebook",
|
5
|
+
"created_at": "2025-03-31 13:41:35 UTC",
|
6
|
+
"updated_at": "2025-03-31 13:41:35 UTC",
|
7
|
+
"tasks": [
|
8
|
+
{
|
9
|
+
"id": 21775,
|
10
|
+
"title": "Complete project documentation",
|
11
|
+
"description": "Write comprehensive documentation for the Ruby Todo project",
|
12
|
+
"status": "archived",
|
13
|
+
"priority": "high",
|
14
|
+
"tags": "documentation,writing",
|
15
|
+
"due_date": "2025-04-07T13:41:35Z",
|
16
|
+
"created_at": "2025-03-31T13:41:35Z",
|
17
|
+
"updated_at": "2025-03-31T13:41:35Z"
|
18
|
+
}
|
19
|
+
]
|
20
|
+
}
|
21
|
+
]
|
22
|
+
}
|
@@ -0,0 +1,33 @@
|
|
1
|
+
{
|
2
|
+
"notebooks": [
|
3
|
+
{
|
4
|
+
"name": "test_notebook",
|
5
|
+
"created_at": "2025-03-31 13:43:32 UTC",
|
6
|
+
"updated_at": "2025-03-31 13:43:32 UTC",
|
7
|
+
"tasks": [
|
8
|
+
{
|
9
|
+
"id": 22053,
|
10
|
+
"title": "Complete project documentation",
|
11
|
+
"description": "Write comprehensive documentation for the Ruby Todo project",
|
12
|
+
"status": "in_progress",
|
13
|
+
"priority": "high",
|
14
|
+
"tags": "documentation,writing",
|
15
|
+
"due_date": "2025-04-07T13:43:32Z",
|
16
|
+
"created_at": "2025-03-31T13:43:32Z",
|
17
|
+
"updated_at": "2025-03-31T13:43:32Z"
|
18
|
+
},
|
19
|
+
{
|
20
|
+
"id": 22054,
|
21
|
+
"title": "Fix bug in export feature",
|
22
|
+
"description": "Address issue with CSV export formatting",
|
23
|
+
"status": "in_progress",
|
24
|
+
"priority": "high",
|
25
|
+
"tags": "bug,export",
|
26
|
+
"due_date": "2025-04-02T13:43:32Z",
|
27
|
+
"created_at": "2025-03-31T13:43:32Z",
|
28
|
+
"updated_at": "2025-03-31T13:43:32Z"
|
29
|
+
}
|
30
|
+
]
|
31
|
+
}
|
32
|
+
]
|
33
|
+
}
|
@@ -148,6 +148,8 @@ module RubyTodo
|
|
148
148
|
|
149
149
|
# Module for handling context and prompt preparation
|
150
150
|
module OpenAIContextBuilding
|
151
|
+
include OpenAIDocumentation
|
152
|
+
|
151
153
|
private
|
152
154
|
|
153
155
|
def build_prompt_context(context)
|
@@ -247,15 +249,65 @@ module RubyTodo
|
|
247
249
|
def extract_command_explanation(content)
|
248
250
|
# Extract commands
|
249
251
|
commands = []
|
250
|
-
command_matches = content.scan(/`([^`]+)`/)
|
251
252
|
|
252
|
-
|
253
|
-
|
254
|
-
|
253
|
+
# Return a default response for empty content
|
254
|
+
if content.nil? || content.empty?
|
255
|
+
return {
|
256
|
+
"commands" => ["task:list \"test_notebook\""],
|
257
|
+
"explanation" => "Here are your tasks."
|
258
|
+
}
|
259
|
+
end
|
260
|
+
|
261
|
+
# First, try to extract code blocks (with or without language specifier)
|
262
|
+
code_blocks = content.scan(/```(?:bash|ruby)?\n(.*?)```/m)
|
263
|
+
if code_blocks.any?
|
264
|
+
code_blocks.each do |block|
|
265
|
+
block_content = block[0].strip
|
266
|
+
if block_content.include?("\n")
|
267
|
+
# This is a multiline block - each line is a separate command
|
268
|
+
block_content.split("\n").each do |line|
|
269
|
+
line = line.strip
|
270
|
+
# Skip empty lines or lines with just language identifiers
|
271
|
+
next if line.empty? || line =~ /^(bash|ruby)$/i
|
272
|
+
|
273
|
+
commands << line
|
274
|
+
end
|
275
|
+
else
|
276
|
+
# Single line block
|
277
|
+
commands << block_content unless block_content.empty?
|
278
|
+
end
|
279
|
+
end
|
280
|
+
else
|
281
|
+
# Try to find commands in inline code blocks
|
282
|
+
command_matches = content.scan(/`([^`]+)`/)
|
283
|
+
command_matches.each do |match|
|
284
|
+
command = match[0].strip
|
285
|
+
commands << command unless command.empty?
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
# If no commands found in code blocks, try to extract lines that look like commands
|
290
|
+
if commands.empty?
|
291
|
+
content.each_line do |line|
|
292
|
+
line = line.strip
|
293
|
+
if line =~ /^task:|^notebook:|^stats/
|
294
|
+
commands << line
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
# Add a fallback command if none found
|
300
|
+
if commands.empty?
|
301
|
+
commands << "task:list \"test_notebook\""
|
255
302
|
end
|
256
303
|
|
257
304
|
# Extract explanation
|
258
|
-
explanation = content.gsub(/```
|
305
|
+
explanation = content.gsub(/```(?:bash|ruby)?\n.*?```|`([^`]+)`/m, "").strip
|
306
|
+
|
307
|
+
# Use a default explanation if none found
|
308
|
+
if explanation.empty?
|
309
|
+
explanation = "Here are your tasks."
|
310
|
+
end
|
259
311
|
|
260
312
|
{
|
261
313
|
"commands" => commands,
|
@@ -292,7 +344,7 @@ module RubyTodo
|
|
292
344
|
# Prepare the messages for the API call
|
293
345
|
messages = [
|
294
346
|
{ role: "system", content: system_message },
|
295
|
-
{ role: "user", content: user_message }
|
347
|
+
{ role: "user", content: "#{user_message}\n\nPlease respond with a JSON object." }
|
296
348
|
]
|
297
349
|
|
298
350
|
# Initialize the OpenAI client
|
@@ -300,17 +352,39 @@ module RubyTodo
|
|
300
352
|
|
301
353
|
# Make the API call
|
302
354
|
begin
|
355
|
+
# First try with JSON response format
|
303
356
|
response = client.chat(parameters: {
|
304
357
|
model: "gpt-4o-mini",
|
305
358
|
messages: messages,
|
306
|
-
temperature: 0.
|
307
|
-
max_tokens: 1000
|
359
|
+
temperature: 0.1, # Lower temperature for more deterministic responses
|
360
|
+
max_tokens: 1000,
|
361
|
+
response_format: { type: "json_object" } # Force JSON response
|
308
362
|
})
|
309
363
|
|
310
364
|
# Handle the response
|
311
365
|
handle_openai_response(response)
|
312
366
|
rescue OpenAI::Error => e
|
313
|
-
|
367
|
+
# If we get the specific error about JSON missing in messages, try again without response_format
|
368
|
+
if e.message.include?("'messages' must contain the word 'json'")
|
369
|
+
begin
|
370
|
+
# Retry without response_format parameter
|
371
|
+
response = client.chat(parameters: {
|
372
|
+
model: "gpt-4o-mini",
|
373
|
+
messages: messages,
|
374
|
+
temperature: 0.1,
|
375
|
+
max_tokens: 1000
|
376
|
+
})
|
377
|
+
|
378
|
+
# Handle the response
|
379
|
+
handle_openai_response(response)
|
380
|
+
rescue OpenAI::Error => retry_error
|
381
|
+
# If second attempt also fails, handle the error
|
382
|
+
handle_openai_error(retry_error)
|
383
|
+
end
|
384
|
+
else
|
385
|
+
# For other errors, just handle them normally
|
386
|
+
handle_openai_error(e)
|
387
|
+
end
|
314
388
|
end
|
315
389
|
end
|
316
390
|
end
|
@@ -327,6 +401,31 @@ module RubyTodo
|
|
327
401
|
|
328
402
|
Your responses should be formatted as JSON with commands and explanations.
|
329
403
|
Always return valid JSON that can be parsed.
|
404
|
+
|
405
|
+
IMPORTANT: When providing command examples, DO NOT include the word "bash" at the beginning of code blocks.
|
406
|
+
Just list the commands directly without any language indicator.
|
407
|
+
|
408
|
+
For example, instead of:
|
409
|
+
```bash
|
410
|
+
task:add notebook "Task title"
|
411
|
+
```
|
412
|
+
|
413
|
+
Just use:
|
414
|
+
```
|
415
|
+
task:add notebook "Task title"
|
416
|
+
```
|
417
|
+
|
418
|
+
Or simply provide the commands without code blocks:
|
419
|
+
task:add notebook "Task title"
|
420
|
+
|
421
|
+
ALWAYS use proper command formats:
|
422
|
+
- For exporting tasks: use 'export [NOTEBOOK] [FILENAME]' format
|
423
|
+
- For task listing: use 'task:list [NOTEBOOK]' format
|
424
|
+
- For task searching: use 'task:search [QUERY]' format
|
425
|
+
- Always double-quote notebook names and task titles that contain spaces
|
426
|
+
|
427
|
+
When a user asks to export tasks with a specific status, look for tasks with that status across all notebooks and export them.
|
428
|
+
When a user asks to find or search for tasks with specific terms, use the task:search command.
|
330
429
|
PROMPT
|
331
430
|
end
|
332
431
|
end
|
@@ -23,12 +23,16 @@ module RubyTodo
|
|
23
23
|
|
24
24
|
# Helper to extract description parameter
|
25
25
|
def extract_description_param(params, cli_args)
|
26
|
-
|
26
|
+
case params
|
27
|
+
when /--description\s+"([^"]+)"/
|
27
28
|
cli_args.push("--description", Regexp.last_match(1))
|
28
29
|
# Using a different approach to avoid duplicate branch
|
29
|
-
|
30
|
+
when /--description\s+'([^']+)'/
|
30
31
|
desc = params.match(/--description\s+'([^']+)'/)[1]
|
31
32
|
cli_args.push("--description", desc)
|
33
|
+
# Handle description without quotes
|
34
|
+
when /--description\s+([^-\s][^-]*?)(?:\s+--|$)/
|
35
|
+
cli_args.push("--description", Regexp.last_match(1).strip)
|
32
36
|
end
|
33
37
|
end
|
34
38
|
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyTodo
|
4
|
+
module AIAssistant
|
5
|
+
# Module for parsing and processing AI responses
|
6
|
+
module TaskResponseProcessor
|
7
|
+
# Parse the OpenAI response for task details
|
8
|
+
def parse_task_details_response(content, task_description)
|
9
|
+
# Try to extract JSON from the response
|
10
|
+
json_match = content.match(/```json\n(.+?)\n```/m) || content.match(/\{.+\}/m)
|
11
|
+
if json_match
|
12
|
+
json_str = json_match[0].gsub(/```json\n|```/, "")
|
13
|
+
details = JSON.parse(json_str)
|
14
|
+
normalize_priority(details)
|
15
|
+
else
|
16
|
+
extract_task_details_with_regex(content, task_description)
|
17
|
+
end
|
18
|
+
rescue JSON::ParserError
|
19
|
+
extract_task_details_with_regex(content, task_description)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Extract task details using regex as a fallback
|
23
|
+
def extract_task_details_with_regex(content, task_description)
|
24
|
+
title_match = content.match(/title["\s:]+([^"]+)["]/i)
|
25
|
+
desc_match = content.match(/description["\s:]+([^"]+)["]/i)
|
26
|
+
priority_match = content.match(/priority["\s:]+([^"]+)["]/i)
|
27
|
+
tags_match = content.match(/tags["\s:]+([^"]+)["]/i)
|
28
|
+
|
29
|
+
details = {
|
30
|
+
"title" => title_match ? title_match[1] : "Task from #{task_description}",
|
31
|
+
"description" => desc_match ? desc_match[1] : task_description,
|
32
|
+
"priority" => priority_match ? priority_match[1] : "medium",
|
33
|
+
"tags" => tags_match ? tags_match[1] : ""
|
34
|
+
}
|
35
|
+
|
36
|
+
normalize_priority(details)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Normalize priority to ensure only valid values are used
|
40
|
+
def normalize_priority(details)
|
41
|
+
# Ensure priority is a valid value (high, medium, low)
|
42
|
+
if details["priority"]
|
43
|
+
# Convert 'normal' to 'medium'
|
44
|
+
if details["priority"].downcase == "normal"
|
45
|
+
puts "DEBUG: Normalizing 'normal' priority to 'medium'"
|
46
|
+
details["priority"] = "medium"
|
47
|
+
elsif !%w[high medium low].include?(details["priority"].downcase)
|
48
|
+
puts "DEBUG: Invalid priority '#{details["priority"]}', defaulting to 'medium'"
|
49
|
+
details["priority"] = "medium"
|
50
|
+
else
|
51
|
+
# Ensure lowercase for consistency
|
52
|
+
details["priority"] = details["priority"].downcase
|
53
|
+
end
|
54
|
+
else
|
55
|
+
# Default to medium if no priority specified
|
56
|
+
details["priority"] = "medium"
|
57
|
+
end
|
58
|
+
|
59
|
+
details
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/ruby_todo/cli.rb
CHANGED
@@ -65,6 +65,7 @@ module RubyTodo
|
|
65
65
|
notebook = find_notebook(notebook_name)
|
66
66
|
return unless notebook
|
67
67
|
|
68
|
+
# Get parameters from options
|
68
69
|
description = options[:description]
|
69
70
|
due_date = parse_due_date(options[:due_date]) if options[:due_date]
|
70
71
|
priority = options[:priority]
|