aia 0.9.23 → 0.9.24
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/.version +1 -1
- data/CHANGELOG.md +11 -0
- data/README.md +8 -1
- data/docs/directives-reference.md +28 -8
- data/docs/index.md +2 -2
- data/lib/aia/config/base.rb +20 -0
- data/lib/aia/directives/checkpoint.rb +283 -0
- data/lib/aia/directives/configuration.rb +3 -88
- data/lib/aia/directives/models.rb +12 -5
- data/lib/aia/directives/registry.rb +2 -0
- data/lib/aia/directives/utility.rb +23 -8
- data/lib/aia/ruby_llm_adapter.rb +20 -4
- data/lib/aia/session.rb +40 -148
- data/lib/aia/topic_context.rb +125 -0
- metadata +4 -3
- data/lib/aia/context_manager.rb +0 -134
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f9592d9ea1bbbbdc04f92dc822cfbdae91c1e04084530583e50f6cdb8a90460d
|
|
4
|
+
data.tar.gz: 897e53ec02de0fcc46a0e5ddcf15ca13a462b7cc20e4f355950f5872e5b04f00
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5e8b63a8ca892346e4dd1090ef487e4fc6dc6b9395238ecc6208128d8908a5dca87d730a9193a15b05dbb005b8d9eb84e2bb7f63100982130eeec69e0bb9483d
|
|
7
|
+
data.tar.gz: 660c1c40117300223a7bd88c67ec826225a44ada72d459ee6c8fd3e3c015e81a7874a9618220c4dc7caf3de930ee140d29ef1c7c8dfc205ecfeedef8e629b36a
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.9.
|
|
1
|
+
0.9.24
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
## [Unreleased]
|
|
3
3
|
|
|
4
|
+
## [0.9.24] 2025-12-17
|
|
5
|
+
### Fixes
|
|
6
|
+
- Ran into a problem with the `shared_tools` gem and the --require parameter of AIA which required changes to both gems.
|
|
7
|
+
|
|
8
|
+
### Improvements
|
|
9
|
+
- **`//tools` Directive Filter**: Added optional filter parameter to the `//tools` directive
|
|
10
|
+
- Filter tools by name substring (case-insensitive)
|
|
11
|
+
- Example: `//tools error` lists only tools with "error" in the name
|
|
12
|
+
- Shows "No tools match the filter: [filter]" when no matches found
|
|
13
|
+
- Header indicates when filtering is active: "Available Tools (filtered by 'filter')"
|
|
14
|
+
|
|
4
15
|
## [0.9.23] 2025-12-06
|
|
5
16
|
|
|
6
17
|
### New Features
|
data/README.md
CHANGED
|
@@ -351,7 +351,7 @@ Directives are special commands in prompt files that begin with `//` and provide
|
|
|
351
351
|
| `//help` | Show available directives | `//help` |
|
|
352
352
|
| `//model` | Show current model configuration | `//model` |
|
|
353
353
|
| `//available_models` | List available models | `//available_models` |
|
|
354
|
-
| `//tools` | Show
|
|
354
|
+
| `//tools` | Show available tools (optional filter by name) | `//tools` or `//tools file` |
|
|
355
355
|
| `//review` | Review current context with checkpoint markers | `//review` |
|
|
356
356
|
|
|
357
357
|
Directives can also be used in the interactive chat sessions.
|
|
@@ -1058,6 +1058,13 @@ Available Tools:
|
|
|
1058
1058
|
- htm_query: Execute a query against the HTM database
|
|
1059
1059
|
- htm_insert: Insert a record into HTM
|
|
1060
1060
|
...
|
|
1061
|
+
|
|
1062
|
+
# Filter tools by name (case-insensitive)
|
|
1063
|
+
> //tools github
|
|
1064
|
+
|
|
1065
|
+
Available Tools (filtered by 'github')
|
|
1066
|
+
- github_create_issue: Create a new GitHub issue
|
|
1067
|
+
- github_list_repos: List repositories for the authenticated user
|
|
1061
1068
|
```
|
|
1062
1069
|
|
|
1063
1070
|
#### Troubleshooting MCP Servers
|
|
@@ -214,11 +214,21 @@ Speak text using system text-to-speech (macOS/Linux).
|
|
|
214
214
|
## Utility Directives
|
|
215
215
|
|
|
216
216
|
### `//tools`
|
|
217
|
-
Display available RubyLLM tools.
|
|
217
|
+
Display available RubyLLM tools with optional filtering.
|
|
218
218
|
|
|
219
|
-
**Syntax**: `//tools`
|
|
219
|
+
**Syntax**: `//tools [filter]`
|
|
220
220
|
|
|
221
|
-
**
|
|
221
|
+
**Parameters**:
|
|
222
|
+
- `filter` (optional) - Case-insensitive substring to filter tool names
|
|
223
|
+
|
|
224
|
+
**Examples**:
|
|
225
|
+
```markdown
|
|
226
|
+
//tools # List all available tools
|
|
227
|
+
//tools file # List tools with "file" in the name
|
|
228
|
+
//tools analyzer # List tools with "analyzer" in the name
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Example Output** (unfiltered):
|
|
222
232
|
```
|
|
223
233
|
Available Tools
|
|
224
234
|
===============
|
|
@@ -234,6 +244,21 @@ WebScraper
|
|
|
234
244
|
selectors and filters.
|
|
235
245
|
```
|
|
236
246
|
|
|
247
|
+
**Example Output** (filtered with `//tools file`):
|
|
248
|
+
```
|
|
249
|
+
Available Tools (filtered by 'file')
|
|
250
|
+
====================================
|
|
251
|
+
|
|
252
|
+
FileReader
|
|
253
|
+
----------
|
|
254
|
+
Read and analyze file contents with support for multiple formats
|
|
255
|
+
including text, JSON, YAML, and CSV files.
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Notes**:
|
|
259
|
+
- When no tools match the filter, displays "No tools match the filter: [filter]"
|
|
260
|
+
- Filtering is case-insensitive (e.g., "File", "FILE", and "file" all match)
|
|
261
|
+
|
|
237
262
|
### `//next`
|
|
238
263
|
Set the next prompt to execute in a workflow.
|
|
239
264
|
|
|
@@ -509,11 +534,6 @@ SyntaxError: unexpected token
|
|
|
509
534
|
ERROR: PUREMD_API_KEY is required in order to include a webpage
|
|
510
535
|
```
|
|
511
536
|
|
|
512
|
-
**Missing Context Manager**:
|
|
513
|
-
```
|
|
514
|
-
Error: Context manager not available for //clear directive.
|
|
515
|
-
```
|
|
516
|
-
|
|
517
537
|
### Custom Directives
|
|
518
538
|
|
|
519
539
|
You can extend AIA with custom directives by creating Ruby files that define new directive methods:
|
data/docs/index.md
CHANGED
|
@@ -126,13 +126,13 @@ graph TD
|
|
|
126
126
|
- **AIA::PromptHandler** - Main prompt processing orchestrator
|
|
127
127
|
- **AIA::ChatProcessorService** - Interactive chat session management
|
|
128
128
|
- **AIA::DirectiveProcessor** - Processes embedded directives (`//command params`)
|
|
129
|
-
- **AIA::
|
|
130
|
-
- **AIA::RubyLLMAdapter** - Interfaces with the ruby_llm gem for AI model communication
|
|
129
|
+
- **AIA::RubyLLMAdapter** - Interfaces with the ruby_llm gem for AI model communication (manages conversation history via RubyLLM's Chat.@messages)
|
|
131
130
|
- **AIA::ShellCommandExecutor** - Executes shell commands safely within prompts
|
|
132
131
|
- **AIA::HistoryManager** - Manages prompt parameter history and user input
|
|
133
132
|
- **AIA::UIPresenter** - Terminal output formatting and presentation
|
|
134
133
|
- **AIA::Session** - Manages chat sessions and state
|
|
135
134
|
- **AIA::Fzf** - Fuzzy finder integration for prompt selection
|
|
135
|
+
- **AIA::Directives::Checkpoint** - Manages conversation checkpoints, restore, clear, and review operations
|
|
136
136
|
|
|
137
137
|
### External Dependencies
|
|
138
138
|
|
data/lib/aia/config/base.rb
CHANGED
|
@@ -157,6 +157,9 @@ module AIA
|
|
|
157
157
|
config.require_libs.each do |library|
|
|
158
158
|
begin
|
|
159
159
|
require(library)
|
|
160
|
+
# Check if the library provides tools that need eager loading
|
|
161
|
+
# Convert library name to module constant (e.g., 'shared_tools' -> 'SharedTools')
|
|
162
|
+
eager_load_tools_from_library(library)
|
|
160
163
|
rescue => e
|
|
161
164
|
STDERR.puts "Error loading library '#{library}' #{e.message}"
|
|
162
165
|
exit_on_error = true
|
|
@@ -168,6 +171,23 @@ module AIA
|
|
|
168
171
|
config
|
|
169
172
|
end
|
|
170
173
|
|
|
174
|
+
# Attempt to eager load tools from a required library
|
|
175
|
+
# Libraries that use Zeitwerk need their tools eager loaded so they
|
|
176
|
+
# appear in ObjectSpace for AIA's tool discovery
|
|
177
|
+
def eager_load_tools_from_library(library)
|
|
178
|
+
# Convert library name to module constant (e.g., 'shared_tools' -> 'SharedTools')
|
|
179
|
+
module_name = library.split('/').first.split('_').map(&:capitalize).join
|
|
180
|
+
|
|
181
|
+
begin
|
|
182
|
+
mod = Object.const_get(module_name)
|
|
183
|
+
if mod.respond_to?(:load_all_tools)
|
|
184
|
+
mod.load_all_tools
|
|
185
|
+
end
|
|
186
|
+
rescue NameError
|
|
187
|
+
# Module doesn't exist with expected name, skip
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
171
191
|
def load_tools(config)
|
|
172
192
|
return if config.tool_paths.empty?
|
|
173
193
|
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# lib/aia/directives/checkpoint.rb
|
|
2
|
+
#
|
|
3
|
+
# Checkpoint and restore directives for managing conversation state.
|
|
4
|
+
# Uses RubyLLM's Chat.@messages as the source of truth for conversation history.
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
module AIA
|
|
8
|
+
module Directives
|
|
9
|
+
module Checkpoint
|
|
10
|
+
# Module-level state for checkpoints
|
|
11
|
+
# Note: Using @checkpoint_store to avoid naming conflict with //checkpoints directive
|
|
12
|
+
@checkpoint_store = {}
|
|
13
|
+
@checkpoint_counter = 0
|
|
14
|
+
@last_checkpoint_name = nil
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
attr_accessor :checkpoint_store, :checkpoint_counter, :last_checkpoint_name
|
|
18
|
+
|
|
19
|
+
# Reset all checkpoint state (useful for testing)
|
|
20
|
+
def reset!
|
|
21
|
+
@checkpoint_store = {}
|
|
22
|
+
@checkpoint_counter = 0
|
|
23
|
+
@last_checkpoint_name = nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# //checkpoint [name]
|
|
28
|
+
# Creates a named checkpoint of the current conversation state.
|
|
29
|
+
# If no name is provided, uses an auto-incrementing number.
|
|
30
|
+
def self.checkpoint(args, _unused = nil)
|
|
31
|
+
name = args.empty? ? nil : args.join(' ').strip
|
|
32
|
+
|
|
33
|
+
if name.nil? || name.empty?
|
|
34
|
+
self.checkpoint_counter += 1
|
|
35
|
+
name = checkpoint_counter.to_s
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
chats = get_chats
|
|
39
|
+
return "Error: No active chat sessions found." if chats.nil? || chats.empty?
|
|
40
|
+
|
|
41
|
+
# Deep copy messages from all chats
|
|
42
|
+
first_chat_messages = chats.values.first&.messages || []
|
|
43
|
+
checkpoint_store[name] = {
|
|
44
|
+
messages: chats.transform_values { |chat|
|
|
45
|
+
chat.messages.map { |msg| deep_copy_message(msg) }
|
|
46
|
+
},
|
|
47
|
+
position: first_chat_messages.size,
|
|
48
|
+
created_at: Time.now,
|
|
49
|
+
topic_preview: extract_last_user_message(first_chat_messages)
|
|
50
|
+
}
|
|
51
|
+
self.last_checkpoint_name = name
|
|
52
|
+
|
|
53
|
+
puts "Checkpoint '#{name}' created at position #{checkpoint_store[name][:position]}."
|
|
54
|
+
""
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# //restore [name]
|
|
58
|
+
# Restores the conversation state to a previously saved checkpoint.
|
|
59
|
+
# If no name is provided, restores to the previous checkpoint (one step back).
|
|
60
|
+
def self.restore(args, _unused = nil)
|
|
61
|
+
name = args.empty? ? nil : args.join(' ').strip
|
|
62
|
+
|
|
63
|
+
if name.nil? || name.empty?
|
|
64
|
+
# Find the previous checkpoint (second-to-last by position)
|
|
65
|
+
name = find_previous_checkpoint
|
|
66
|
+
if name.nil?
|
|
67
|
+
return "Error: No previous checkpoint to restore to."
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
unless checkpoint_store.key?(name)
|
|
72
|
+
available = checkpoint_names.empty? ? "none" : checkpoint_names.join(', ')
|
|
73
|
+
return "Error: Checkpoint '#{name}' not found. Available: #{available}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
checkpoint_data = checkpoint_store[name]
|
|
77
|
+
chats = get_chats
|
|
78
|
+
|
|
79
|
+
return "Error: No active chat sessions found." if chats.nil? || chats.empty?
|
|
80
|
+
|
|
81
|
+
# Restore messages to each chat
|
|
82
|
+
checkpoint_data[:messages].each do |model_id, saved_messages|
|
|
83
|
+
chat = chats[model_id]
|
|
84
|
+
next unless chat
|
|
85
|
+
|
|
86
|
+
# Replace the chat's messages with the saved ones
|
|
87
|
+
restored_messages = saved_messages.map { |msg| deep_copy_message(msg) }
|
|
88
|
+
chat.instance_variable_set(:@messages, restored_messages)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Remove checkpoints that are now invalid (position > restored position)
|
|
92
|
+
# because they reference conversation states that no longer exist
|
|
93
|
+
restored_position = checkpoint_data[:position]
|
|
94
|
+
removed_count = remove_invalid_checkpoints(restored_position)
|
|
95
|
+
|
|
96
|
+
# Update last_checkpoint_name to this checkpoint
|
|
97
|
+
self.last_checkpoint_name = name
|
|
98
|
+
|
|
99
|
+
msg = "Context restored to checkpoint '#{name}' (position #{restored_position})."
|
|
100
|
+
msg += " Removed #{removed_count} checkpoint(s) that were beyond this position." if removed_count > 0
|
|
101
|
+
msg
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# //clear
|
|
105
|
+
# Clears the conversation context, optionally keeping the system prompt.
|
|
106
|
+
def self.clear(args, _unused = nil)
|
|
107
|
+
keep_system = !args.include?('--all')
|
|
108
|
+
|
|
109
|
+
chats = get_chats
|
|
110
|
+
return "Error: No active chat sessions found." if chats.nil? || chats.empty?
|
|
111
|
+
|
|
112
|
+
chats.each do |_model_id, chat|
|
|
113
|
+
if keep_system
|
|
114
|
+
system_msg = chat.messages.find { |m| m.role == :system }
|
|
115
|
+
chat.instance_variable_set(:@messages, [])
|
|
116
|
+
chat.add_message(system_msg) if system_msg
|
|
117
|
+
else
|
|
118
|
+
chat.instance_variable_set(:@messages, [])
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Clear all checkpoints
|
|
123
|
+
checkpoint_store.clear
|
|
124
|
+
self.checkpoint_counter = 0
|
|
125
|
+
self.last_checkpoint_name = nil
|
|
126
|
+
|
|
127
|
+
"Chat context cleared."
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# //review
|
|
131
|
+
# Displays the current conversation context with checkpoint markers.
|
|
132
|
+
def self.review(args, _unused = nil)
|
|
133
|
+
chats = get_chats
|
|
134
|
+
return "Error: No active chat sessions found." if chats.nil? || chats.empty?
|
|
135
|
+
|
|
136
|
+
# For multi-model, show first chat's messages (they should be similar for user messages)
|
|
137
|
+
first_chat = chats.values.first
|
|
138
|
+
messages = first_chat&.messages || []
|
|
139
|
+
|
|
140
|
+
puts "\n=== Chat Context (RubyLLM) ==="
|
|
141
|
+
puts "Total messages: #{messages.size}"
|
|
142
|
+
puts "Models: #{chats.keys.join(', ')}"
|
|
143
|
+
puts "Checkpoints: #{checkpoint_names.join(', ')}" if checkpoint_names.any?
|
|
144
|
+
puts
|
|
145
|
+
|
|
146
|
+
positions = checkpoint_positions
|
|
147
|
+
|
|
148
|
+
messages.each_with_index do |msg, index|
|
|
149
|
+
# Show checkpoint marker if one exists at this position
|
|
150
|
+
if positions[index]
|
|
151
|
+
puts "📍 [Checkpoint: #{positions[index].join(', ')}]"
|
|
152
|
+
puts "-" * 40
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
role = msg.role.to_s.capitalize
|
|
156
|
+
content = format_message_content(msg)
|
|
157
|
+
|
|
158
|
+
puts "#{index + 1}. [#{role}]: #{content}"
|
|
159
|
+
puts
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Check for checkpoint at the end
|
|
163
|
+
if positions[messages.size]
|
|
164
|
+
puts "📍 [Checkpoint: #{positions[messages.size].join(', ')}]"
|
|
165
|
+
puts "-" * 40
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
puts "=== End of Context ==="
|
|
169
|
+
""
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# //checkpoints
|
|
173
|
+
# Lists all available checkpoints with their details.
|
|
174
|
+
def self.checkpoints_list(args, _unused = nil)
|
|
175
|
+
if checkpoint_store.empty?
|
|
176
|
+
puts "No checkpoints available."
|
|
177
|
+
return ""
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
puts "\n=== Available Checkpoints ==="
|
|
181
|
+
checkpoint_store.each do |name, data|
|
|
182
|
+
created = data[:created_at]&.strftime('%H:%M:%S') || 'unknown'
|
|
183
|
+
puts " #{name}: position #{data[:position]}, created #{created}"
|
|
184
|
+
if data[:topic_preview] && !data[:topic_preview].empty?
|
|
185
|
+
puts " → \"#{data[:topic_preview]}\""
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
puts "=== End of Checkpoints ==="
|
|
189
|
+
""
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Helper methods
|
|
193
|
+
def self.checkpoint_names
|
|
194
|
+
checkpoint_store.keys
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def self.checkpoint_positions
|
|
198
|
+
positions = {}
|
|
199
|
+
checkpoint_store.each do |name, data|
|
|
200
|
+
pos = data[:position]
|
|
201
|
+
positions[pos] ||= []
|
|
202
|
+
positions[pos] << name
|
|
203
|
+
end
|
|
204
|
+
positions
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Remove checkpoints with position > the given position
|
|
208
|
+
# Returns the count of removed checkpoints
|
|
209
|
+
def self.remove_invalid_checkpoints(max_position)
|
|
210
|
+
invalid_names = checkpoint_store.select { |_name, data| data[:position] > max_position }.keys
|
|
211
|
+
invalid_names.each { |name| checkpoint_store.delete(name) }
|
|
212
|
+
invalid_names.size
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Find the previous checkpoint (second-to-last by position)
|
|
216
|
+
# This is used when //restore is called without a name
|
|
217
|
+
def self.find_previous_checkpoint
|
|
218
|
+
return nil if checkpoint_store.size < 2
|
|
219
|
+
|
|
220
|
+
# Sort checkpoints by position descending, return the second one
|
|
221
|
+
sorted = checkpoint_store.sort_by { |_name, data| -data[:position] }
|
|
222
|
+
sorted[1]&.first # Return the name of the second-to-last checkpoint
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
private
|
|
226
|
+
|
|
227
|
+
def self.get_chats
|
|
228
|
+
return nil unless AIA.client.respond_to?(:chats)
|
|
229
|
+
|
|
230
|
+
AIA.client.chats
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def self.deep_copy_message(msg)
|
|
234
|
+
RubyLLM::Message.new(
|
|
235
|
+
role: msg.role,
|
|
236
|
+
content: msg.content,
|
|
237
|
+
tool_calls: msg.tool_calls&.transform_values { |tc| tc.dup rescue tc },
|
|
238
|
+
tool_call_id: msg.tool_call_id,
|
|
239
|
+
input_tokens: msg.input_tokens,
|
|
240
|
+
output_tokens: msg.output_tokens,
|
|
241
|
+
model_id: msg.model_id
|
|
242
|
+
)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def self.format_message_content(msg)
|
|
246
|
+
if msg.tool_call?
|
|
247
|
+
tool_names = msg.tool_calls.values.map(&:name).join(', ')
|
|
248
|
+
"[Tool calls: #{tool_names}]"
|
|
249
|
+
elsif msg.tool_result?
|
|
250
|
+
result_preview = msg.content.to_s[0..50]
|
|
251
|
+
"[Tool result for: #{msg.tool_call_id}] #{result_preview}..."
|
|
252
|
+
else
|
|
253
|
+
content = msg.content.to_s
|
|
254
|
+
content.length > 150 ? "#{content[0..147]}..." : content
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Extract a preview of the last user message for checkpoint context
|
|
259
|
+
def self.extract_last_user_message(messages, max_length: 70)
|
|
260
|
+
return "" if messages.nil? || messages.empty?
|
|
261
|
+
|
|
262
|
+
# Find the last user message (not system, not assistant, not tool)
|
|
263
|
+
last_user_msg = messages.reverse.find { |msg| msg.role == :user }
|
|
264
|
+
return "" unless last_user_msg
|
|
265
|
+
|
|
266
|
+
content = last_user_msg.content.to_s.strip
|
|
267
|
+
# Collapse whitespace and truncate
|
|
268
|
+
content = content.gsub(/\s+/, ' ')
|
|
269
|
+
content.length > max_length ? "#{content[0..max_length - 4]}..." : content
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Aliases
|
|
273
|
+
class << self
|
|
274
|
+
alias_method :ckp, :checkpoint
|
|
275
|
+
alias_method :cp, :checkpoint
|
|
276
|
+
alias_method :context, :review
|
|
277
|
+
# Route //checkpoints directive to checkpoints_list
|
|
278
|
+
# (safe now that we renamed attr_accessor to checkpoint_store)
|
|
279
|
+
alias_method :checkpoints, :checkpoints_list
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
@@ -88,100 +88,15 @@ module AIA
|
|
|
88
88
|
send(:config, args.prepend('top_p'), context_manager)
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
context_manager.clear_context
|
|
97
|
-
''
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def self.review(args, context_manager = nil)
|
|
101
|
-
return "Error: Context manager not available for //review directive." if context_manager.nil?
|
|
102
|
-
|
|
103
|
-
context = context_manager.get_context
|
|
104
|
-
checkpoint_positions = context_manager.checkpoint_positions
|
|
105
|
-
|
|
106
|
-
# Display context with checkpoint markers
|
|
107
|
-
puts "\n=== Chat Context ==="
|
|
108
|
-
puts "Total messages: #{context.size}"
|
|
109
|
-
|
|
110
|
-
if checkpoint_positions.any?
|
|
111
|
-
puts "Checkpoints: #{context_manager.checkpoint_names.join(', ')}"
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
puts "\n"
|
|
115
|
-
|
|
116
|
-
context.each_with_index do |message, index|
|
|
117
|
-
# Check if there's a checkpoint at this position
|
|
118
|
-
if checkpoint_positions[index]
|
|
119
|
-
checkpoint_names = checkpoint_positions[index].join(', ')
|
|
120
|
-
puts "📍 [Checkpoint: #{checkpoint_names}]"
|
|
121
|
-
puts "-" * 40
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# Display the message
|
|
125
|
-
role_display = message[:role].capitalize
|
|
126
|
-
content_preview = message[:content].to_s
|
|
127
|
-
|
|
128
|
-
# Truncate long content for display
|
|
129
|
-
if content_preview.length > 200
|
|
130
|
-
content_preview = content_preview[0..197] + "..."
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
puts "#{index + 1}. [#{role_display}]: #{content_preview}"
|
|
134
|
-
puts ""
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# Check if there's a checkpoint at the end (after all messages)
|
|
138
|
-
if checkpoint_positions[context.size]
|
|
139
|
-
checkpoint_names = checkpoint_positions[context.size].join(', ')
|
|
140
|
-
puts "📍 [Checkpoint: #{checkpoint_names}]"
|
|
141
|
-
puts "-" * 40
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
puts "=== End of Context ==="
|
|
145
|
-
''
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def self.checkpoint(args, context_manager = nil)
|
|
149
|
-
if context_manager.nil?
|
|
150
|
-
return "Error: Context manager not available for //checkpoint directive."
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
name = args.empty? ? nil : args.join(' ').strip
|
|
154
|
-
checkpoint_name = context_manager.create_checkpoint(name: name)
|
|
155
|
-
puts "Checkpoint '#{checkpoint_name}' created."
|
|
156
|
-
""
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def self.restore(args, context_manager = nil)
|
|
160
|
-
if context_manager.nil?
|
|
161
|
-
return "Error: Context manager not available for //restore directive."
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
name = args.empty? ? nil : args.join(' ').strip
|
|
165
|
-
|
|
166
|
-
if context_manager.restore_checkpoint(name: name)
|
|
167
|
-
restored_name = name || context_manager.checkpoint_names.last
|
|
168
|
-
"Context restored to checkpoint '#{restored_name}'."
|
|
169
|
-
else
|
|
170
|
-
if name
|
|
171
|
-
"Error: Checkpoint '#{name}' not found. Available checkpoints: #{context_manager.checkpoint_names.join(', ')}"
|
|
172
|
-
else
|
|
173
|
-
"Error: No checkpoints available to restore."
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
end
|
|
91
|
+
# NOTE: clear, review, checkpoint, and restore directives have been moved to
|
|
92
|
+
# lib/aia/directives/checkpoint.rb which uses RubyLLM's Chat.@messages
|
|
93
|
+
# as the source of truth for conversation history.
|
|
177
94
|
|
|
178
95
|
# Set up aliases - these work on the module's singleton class
|
|
179
96
|
class << self
|
|
180
97
|
alias_method :cfg, :config
|
|
181
98
|
alias_method :temp, :temperature
|
|
182
99
|
alias_method :topp, :top_p
|
|
183
|
-
alias_method :context, :review
|
|
184
|
-
alias_method :ckp, :checkpoint
|
|
185
100
|
end
|
|
186
101
|
end
|
|
187
102
|
end
|
|
@@ -231,9 +231,10 @@ module AIA
|
|
|
231
231
|
'review' => 'Display the current conversation context with checkpoint markers',
|
|
232
232
|
'checkpoint' => 'Create a named checkpoint of the current context',
|
|
233
233
|
'restore' => 'Restore context to a previous checkpoint',
|
|
234
|
+
'checkpoints_list' => 'List all available checkpoints',
|
|
234
235
|
|
|
235
236
|
# Utility directives
|
|
236
|
-
'tools' => 'List available tools',
|
|
237
|
+
'tools' => 'List available tools (optional filter by name substring)',
|
|
237
238
|
'next' => 'Set the next prompt in the sequence',
|
|
238
239
|
'pipeline' => 'Set or view the prompt workflow sequence',
|
|
239
240
|
'terse' => 'Add instruction for concise responses',
|
|
@@ -261,6 +262,7 @@ module AIA
|
|
|
261
262
|
'temp' => nil, # alias for temperature
|
|
262
263
|
'topp' => nil, # alias for top_p
|
|
263
264
|
'context' => nil, # alias for review
|
|
265
|
+
'ckp' => nil, # alias for checkpoint
|
|
264
266
|
'cp' => nil, # alias for checkpoint
|
|
265
267
|
'workflow' => nil, # alias for pipeline
|
|
266
268
|
'rb' => nil, # alias for ruby
|
|
@@ -282,13 +284,18 @@ module AIA
|
|
|
282
284
|
AIA::Directives::Utility,
|
|
283
285
|
AIA::Directives::Configuration,
|
|
284
286
|
AIA::Directives::Execution,
|
|
285
|
-
AIA::Directives::Models
|
|
287
|
+
AIA::Directives::Models,
|
|
288
|
+
AIA::Directives::Checkpoint
|
|
286
289
|
]
|
|
287
290
|
|
|
288
291
|
all_directives = {}
|
|
289
292
|
excluded_methods = ['run', 'initialize', 'private?', 'descriptions', 'aliases', 'build_aliases',
|
|
290
293
|
'desc', 'method_added', 'register_directive_module', 'process',
|
|
291
|
-
'directive?', 'prefix_size'
|
|
294
|
+
'directive?', 'prefix_size', 'reset!', 'checkpoint_names',
|
|
295
|
+
'checkpoint_positions', 'get_chats', 'deep_copy_message',
|
|
296
|
+
'format_message_content', 'checkpoints', 'checkpoint_counter',
|
|
297
|
+
'last_checkpoint_name', 'checkpoints=', 'checkpoint_counter=',
|
|
298
|
+
'last_checkpoint_name=']
|
|
292
299
|
|
|
293
300
|
# Collect directives from all modules
|
|
294
301
|
all_modules.each do |mod|
|
|
@@ -314,7 +321,7 @@ module AIA
|
|
|
314
321
|
'temperature' => ['temp'],
|
|
315
322
|
'top_p' => ['topp'],
|
|
316
323
|
'review' => ['context'],
|
|
317
|
-
'checkpoint' => ['cp'],
|
|
324
|
+
'checkpoint' => ['ckp', 'cp'],
|
|
318
325
|
'pipeline' => ['workflow'],
|
|
319
326
|
'ruby' => ['rb'],
|
|
320
327
|
'shell' => ['sh'],
|
|
@@ -333,7 +340,7 @@ module AIA
|
|
|
333
340
|
|
|
334
341
|
# Sort and display directives by category
|
|
335
342
|
categories = {
|
|
336
|
-
'Configuration' => ['config', 'model', 'temperature', 'top_p', 'clear', 'review', 'checkpoint', 'restore'],
|
|
343
|
+
'Configuration' => ['config', 'model', 'temperature', 'top_p', 'clear', 'review', 'checkpoint', 'restore', 'checkpoints_list'],
|
|
337
344
|
'Utility' => ['tools', 'next', 'pipeline', 'terse', 'robot', 'help'],
|
|
338
345
|
'Execution' => ['ruby', 'shell', 'say'],
|
|
339
346
|
'Web & Files' => ['webpage', 'include'],
|
|
@@ -5,6 +5,7 @@ require_relative 'utility'
|
|
|
5
5
|
require_relative 'configuration'
|
|
6
6
|
require_relative 'execution'
|
|
7
7
|
require_relative 'models'
|
|
8
|
+
require_relative 'checkpoint'
|
|
8
9
|
|
|
9
10
|
module AIA
|
|
10
11
|
module Directives
|
|
@@ -115,6 +116,7 @@ module AIA
|
|
|
115
116
|
register_directive_module(Configuration)
|
|
116
117
|
register_directive_module(Execution)
|
|
117
118
|
register_directive_module(Models)
|
|
119
|
+
register_directive_module(Checkpoint)
|
|
118
120
|
end
|
|
119
121
|
end
|
|
120
122
|
end
|
|
@@ -12,19 +12,34 @@ module AIA
|
|
|
12
12
|
indent = 4
|
|
13
13
|
spaces = " " * indent
|
|
14
14
|
width = TTY::Screen.width - indent - 2
|
|
15
|
+
filter = args.first&.downcase
|
|
15
16
|
|
|
16
17
|
if AIA.config.tools.empty?
|
|
17
18
|
puts "No tools are available"
|
|
18
19
|
else
|
|
19
|
-
|
|
20
|
-
puts "Available Tools"
|
|
21
|
-
puts "==============="
|
|
20
|
+
tools_to_display = AIA.config.tools
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
if filter
|
|
23
|
+
tools_to_display = tools_to_display.select do |tool|
|
|
24
|
+
name = tool.respond_to?(:name) ? tool.name : tool.class.name
|
|
25
|
+
name.downcase.include?(filter)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
if tools_to_display.empty?
|
|
30
|
+
puts "No tools match the filter: #{args.first}"
|
|
31
|
+
else
|
|
32
|
+
puts
|
|
33
|
+
header = filter ? "Available Tools (filtered by '#{args.first}')" : "Available Tools"
|
|
34
|
+
puts header
|
|
35
|
+
puts "=" * header.length
|
|
36
|
+
|
|
37
|
+
tools_to_display.each do |tool|
|
|
38
|
+
name = tool.respond_to?(:name) ? tool.name : tool.class.name
|
|
39
|
+
puts "\n#{name}"
|
|
40
|
+
puts "-" * name.size
|
|
41
|
+
puts WordWrapper::MinimumRaggedness.new(width, tool.description).wrap.split("\n").map { |s| spaces + s + "\n" }.join
|
|
42
|
+
end
|
|
28
43
|
end
|
|
29
44
|
end
|
|
30
45
|
puts
|
data/lib/aia/ruby_llm_adapter.rb
CHANGED
|
@@ -5,7 +5,7 @@ require_relative '../extensions/ruby_llm/provider_fix'
|
|
|
5
5
|
|
|
6
6
|
module AIA
|
|
7
7
|
class RubyLLMAdapter
|
|
8
|
-
attr_reader :tools, :model_specs
|
|
8
|
+
attr_reader :tools, :model_specs, :chats
|
|
9
9
|
|
|
10
10
|
def initialize
|
|
11
11
|
@model_specs = extract_models_config # Full specs with role info
|
|
@@ -228,7 +228,17 @@ module AIA
|
|
|
228
228
|
|
|
229
229
|
def support_local_tools
|
|
230
230
|
@tools += ObjectSpace.each_object(Class).select do |klass|
|
|
231
|
-
klass < RubyLLM::Tool
|
|
231
|
+
next false unless klass < RubyLLM::Tool
|
|
232
|
+
|
|
233
|
+
# Filter out tools that can't be instantiated without arguments
|
|
234
|
+
# RubyLLM calls tool.new without args, so we must verify each tool works
|
|
235
|
+
begin
|
|
236
|
+
klass.new
|
|
237
|
+
true
|
|
238
|
+
rescue ArgumentError, LoadError, StandardError
|
|
239
|
+
# Skip tools that require arguments or have missing dependencies
|
|
240
|
+
false
|
|
241
|
+
end
|
|
232
242
|
end
|
|
233
243
|
end
|
|
234
244
|
|
|
@@ -634,7 +644,10 @@ module AIA
|
|
|
634
644
|
|
|
635
645
|
@tools.select! do |tool|
|
|
636
646
|
tool_name = tool.respond_to?(:name) ? tool.name : tool.class.name
|
|
637
|
-
AIA.config.allowed_tools
|
|
647
|
+
AIA.config.allowed_tools
|
|
648
|
+
.split(',')
|
|
649
|
+
.map(&:strip)
|
|
650
|
+
.any? { |allowed| tool_name.include?(allowed) }
|
|
638
651
|
end
|
|
639
652
|
end
|
|
640
653
|
|
|
@@ -644,7 +657,10 @@ module AIA
|
|
|
644
657
|
|
|
645
658
|
@tools.reject! do |tool|
|
|
646
659
|
tool_name = tool.respond_to?(:name) ? tool.name : tool.class.name
|
|
647
|
-
AIA.config.rejected_tools
|
|
660
|
+
AIA.config.rejected_tools
|
|
661
|
+
.split(',')
|
|
662
|
+
.map(&:strip)
|
|
663
|
+
.any? { |rejected| tool_name.include?(rejected) }
|
|
648
664
|
end
|
|
649
665
|
end
|
|
650
666
|
|
data/lib/aia/session.rb
CHANGED
|
@@ -9,7 +9,6 @@ require "fileutils"
|
|
|
9
9
|
require "amazing_print"
|
|
10
10
|
require_relative "directive_processor"
|
|
11
11
|
require_relative "history_manager"
|
|
12
|
-
require_relative "context_manager"
|
|
13
12
|
require_relative "ui_presenter"
|
|
14
13
|
require_relative "chat_processor_service"
|
|
15
14
|
require_relative "prompt_handler"
|
|
@@ -45,28 +44,9 @@ module AIA
|
|
|
45
44
|
end
|
|
46
45
|
|
|
47
46
|
def initialize_components
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
@context_managers = {}
|
|
52
|
-
AIA.config.model.each do |model_spec|
|
|
53
|
-
# Handle both old string format and new hash format (ADR-005)
|
|
54
|
-
internal_id = if model_spec.is_a?(Hash)
|
|
55
|
-
model_spec[:internal_id]
|
|
56
|
-
else
|
|
57
|
-
model_spec
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
@context_managers[internal_id] = ContextManager.new(
|
|
61
|
-
system_prompt: AIA.config.system_prompt
|
|
62
|
-
)
|
|
63
|
-
end
|
|
64
|
-
@context_manager = nil # Signal we're using per-model managers
|
|
65
|
-
else
|
|
66
|
-
@context_manager = ContextManager.new(system_prompt: AIA.config.system_prompt)
|
|
67
|
-
@context_managers = nil
|
|
68
|
-
end
|
|
69
|
-
|
|
47
|
+
# RubyLLM's Chat instances maintain conversation history internally
|
|
48
|
+
# via @messages array. No separate context manager needed.
|
|
49
|
+
# Checkpoint/restore directives access Chat.@messages directly via AIA.client.chats
|
|
70
50
|
@ui_presenter = UIPresenter.new
|
|
71
51
|
@directive_processor = DirectiveProcessor.new
|
|
72
52
|
@chat_processor = ChatProcessorService.new(@ui_presenter, @directive_processor)
|
|
@@ -225,23 +205,21 @@ module AIA
|
|
|
225
205
|
end
|
|
226
206
|
|
|
227
207
|
# Send prompt to AI and handle the response
|
|
208
|
+
# RubyLLM's Chat automatically adds user messages and responses to its internal @messages
|
|
228
209
|
def send_prompt_and_get_response(prompt_text)
|
|
229
|
-
#
|
|
230
|
-
@context_manager.add_to_context(role: "user", content: prompt_text)
|
|
231
|
-
|
|
232
|
-
# Process the prompt
|
|
210
|
+
# Process the prompt - RubyLLM Chat maintains conversation history internally
|
|
233
211
|
@ui_presenter.display_thinking_animation
|
|
234
|
-
|
|
212
|
+
response_data = @chat_processor.process_prompt(prompt_text)
|
|
235
213
|
|
|
236
|
-
#
|
|
237
|
-
|
|
214
|
+
# Handle response format (may include metrics)
|
|
215
|
+
content = response_data.is_a?(Hash) ? response_data[:content] : response_data
|
|
238
216
|
|
|
239
217
|
# Output the response
|
|
240
|
-
@chat_processor.output_response(
|
|
218
|
+
@chat_processor.output_response(content)
|
|
241
219
|
|
|
242
220
|
# Process any directives in the response
|
|
243
|
-
if @directive_processor.directive?(
|
|
244
|
-
directive_result = @directive_processor.process(
|
|
221
|
+
if @directive_processor.directive?(content)
|
|
222
|
+
directive_result = @directive_processor.process(content, nil)
|
|
245
223
|
puts "\nDirective output: #{directive_result}" if directive_result && !directive_result.strip.empty?
|
|
246
224
|
end
|
|
247
225
|
end
|
|
@@ -315,19 +293,16 @@ module AIA
|
|
|
315
293
|
|
|
316
294
|
return if context.empty?
|
|
317
295
|
|
|
318
|
-
#
|
|
319
|
-
@context_manager.add_to_context(role: "user", content: context)
|
|
320
|
-
|
|
321
|
-
# Process the context
|
|
296
|
+
# Process the context - RubyLLM Chat maintains conversation history internally
|
|
322
297
|
@ui_presenter.display_thinking_animation
|
|
323
|
-
|
|
298
|
+
response_data = @chat_processor.process_prompt(context)
|
|
324
299
|
|
|
325
|
-
#
|
|
326
|
-
|
|
300
|
+
# Handle response format (may include metrics)
|
|
301
|
+
content = response_data.is_a?(Hash) ? response_data[:content] : response_data
|
|
327
302
|
|
|
328
303
|
# Output the response
|
|
329
|
-
@chat_processor.output_response(
|
|
330
|
-
@chat_processor.speak(
|
|
304
|
+
@chat_processor.output_response(content)
|
|
305
|
+
@chat_processor.speak(content)
|
|
331
306
|
@ui_presenter.display_separator
|
|
332
307
|
end
|
|
333
308
|
|
|
@@ -347,14 +322,15 @@ module AIA
|
|
|
347
322
|
@chat_prompt.text = piped_input
|
|
348
323
|
processed_input = @chat_prompt.to_s
|
|
349
324
|
|
|
350
|
-
|
|
351
|
-
|
|
325
|
+
# Process the piped input - RubyLLM Chat maintains conversation history internally
|
|
352
326
|
@ui_presenter.display_thinking_animation
|
|
353
|
-
|
|
327
|
+
response_data = @chat_processor.process_prompt(processed_input)
|
|
328
|
+
|
|
329
|
+
# Handle response format (may include metrics)
|
|
330
|
+
content = response_data.is_a?(Hash) ? response_data[:content] : response_data
|
|
354
331
|
|
|
355
|
-
@
|
|
356
|
-
@chat_processor.
|
|
357
|
-
@chat_processor.speak(response) if AIA.speak?
|
|
332
|
+
@chat_processor.output_response(content)
|
|
333
|
+
@chat_processor.speak(content) if AIA.speak?
|
|
358
334
|
@ui_presenter.display_separator
|
|
359
335
|
|
|
360
336
|
STDIN.reopen(original_stdin)
|
|
@@ -389,29 +365,10 @@ module AIA
|
|
|
389
365
|
@chat_prompt.text = follow_up_prompt
|
|
390
366
|
processed_prompt = @chat_prompt.to_s
|
|
391
367
|
|
|
392
|
-
#
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
ctx_mgr.add_to_context(role: "user", content: processed_prompt)
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
# Get per-model conversations
|
|
400
|
-
conversations = {}
|
|
401
|
-
@context_managers.each do |model_name, ctx_mgr|
|
|
402
|
-
conversations[model_name] = ctx_mgr.get_context
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
@ui_presenter.display_thinking_animation
|
|
406
|
-
response_data = @chat_processor.process_prompt(conversations)
|
|
407
|
-
else
|
|
408
|
-
# Single-model: use original logic
|
|
409
|
-
@context_manager.add_to_context(role: "user", content: processed_prompt)
|
|
410
|
-
conversation = @context_manager.get_context
|
|
411
|
-
|
|
412
|
-
@ui_presenter.display_thinking_animation
|
|
413
|
-
response_data = @chat_processor.process_prompt(conversation)
|
|
414
|
-
end
|
|
368
|
+
# Process the prompt - RubyLLM Chat maintains conversation history internally
|
|
369
|
+
# via @messages array. Each model's Chat instance tracks its own conversation.
|
|
370
|
+
@ui_presenter.display_thinking_animation
|
|
371
|
+
response_data = @chat_processor.process_prompt(processed_prompt)
|
|
415
372
|
|
|
416
373
|
# Handle new response format with metrics
|
|
417
374
|
if response_data.is_a?(Hash)
|
|
@@ -437,21 +394,6 @@ module AIA
|
|
|
437
394
|
end
|
|
438
395
|
end
|
|
439
396
|
|
|
440
|
-
# Add responses to context (ADR-002 revised)
|
|
441
|
-
if @context_managers
|
|
442
|
-
# Multi-model: parse combined response and add each model's response to its own context
|
|
443
|
-
parsed_responses = parse_multi_model_response(content)
|
|
444
|
-
parsed_responses.each do |model_name, model_response|
|
|
445
|
-
@context_managers[model_name]&.add_to_context(
|
|
446
|
-
role: "assistant",
|
|
447
|
-
content: model_response
|
|
448
|
-
)
|
|
449
|
-
end
|
|
450
|
-
else
|
|
451
|
-
# Single-model: add response to single context
|
|
452
|
-
@context_manager.add_to_context(role: "assistant", content: content)
|
|
453
|
-
end
|
|
454
|
-
|
|
455
397
|
@chat_processor.speak(content)
|
|
456
398
|
|
|
457
399
|
@ui_presenter.display_separator
|
|
@@ -459,71 +401,21 @@ module AIA
|
|
|
459
401
|
end
|
|
460
402
|
|
|
461
403
|
def process_chat_directive(follow_up_prompt)
|
|
462
|
-
#
|
|
463
|
-
#
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
handle_successful_directive(follow_up_prompt, directive_output)
|
|
473
|
-
end
|
|
474
|
-
|
|
475
|
-
def handle_clear_directive
|
|
476
|
-
# Clear context manager(s) - ADR-002 revised
|
|
477
|
-
if @context_managers
|
|
478
|
-
# Multi-model: clear all context managers
|
|
479
|
-
@context_managers.each_value { |ctx_mgr| ctx_mgr.clear_context(keep_system_prompt: true) }
|
|
480
|
-
else
|
|
481
|
-
# Single-model: clear single context manager
|
|
482
|
-
@context_manager.clear_context(keep_system_prompt: true)
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
# Try clearing the client's context
|
|
486
|
-
if AIA.config.client && AIA.config.client.respond_to?(:clear_context)
|
|
487
|
-
begin
|
|
488
|
-
AIA.config.client.clear_context
|
|
489
|
-
rescue => e
|
|
490
|
-
STDERR.puts "Warning: Error clearing client context: #{e.message}"
|
|
491
|
-
# Continue anyway - the context manager has been cleared which is the main goal
|
|
492
|
-
end
|
|
404
|
+
# Directives now access RubyLLM's Chat.@messages directly via AIA.client
|
|
405
|
+
# The second parameter is no longer used by checkpoint/restore/clear/review
|
|
406
|
+
directive_output = @directive_processor.process(follow_up_prompt, nil)
|
|
407
|
+
|
|
408
|
+
# Checkpoint-related directives (clear, checkpoint, restore, review) handle
|
|
409
|
+
# everything internally via the Checkpoint module, which operates directly
|
|
410
|
+
# on RubyLLM's Chat.@messages - no additional handling needed here.
|
|
411
|
+
if follow_up_prompt.strip.start_with?("//clear", "//checkpoint", "//restore", "//review", "//context")
|
|
412
|
+
@ui_presenter.display_info(directive_output) unless directive_output.nil? || directive_output.strip.empty?
|
|
413
|
+
return nil
|
|
493
414
|
end
|
|
494
415
|
|
|
495
|
-
|
|
496
|
-
# as that could cause termination if model initialization fails
|
|
497
|
-
|
|
498
|
-
@ui_presenter.display_info("Chat context cleared.")
|
|
499
|
-
nil
|
|
500
|
-
end
|
|
501
|
-
|
|
502
|
-
def handle_checkpoint_directive(directive_output)
|
|
503
|
-
@ui_presenter.display_info(directive_output)
|
|
504
|
-
nil
|
|
505
|
-
end
|
|
506
|
-
|
|
507
|
-
def handle_restore_directive(directive_output)
|
|
508
|
-
# If the restore was successful, we also need to refresh the client's context - ADR-002 revised
|
|
509
|
-
if directive_output.start_with?("Context restored")
|
|
510
|
-
# Clear the client's context without reinitializing the entire adapter
|
|
511
|
-
if AIA.config.client && AIA.config.client.respond_to?(:clear_context)
|
|
512
|
-
begin
|
|
513
|
-
AIA.config.client.clear_context
|
|
514
|
-
rescue => e
|
|
515
|
-
STDERR.puts "Warning: Error clearing client context after restore: #{e.message}"
|
|
516
|
-
# Continue anyway - the context manager has been restored which is the main goal
|
|
517
|
-
end
|
|
518
|
-
end
|
|
519
|
-
|
|
520
|
-
# Note: For multi-model, only the first context manager was used for restore
|
|
521
|
-
# This is a limitation of the current directive system
|
|
522
|
-
# TODO: Consider supporting restore for all context managers
|
|
523
|
-
end
|
|
416
|
+
return handle_empty_directive_output if directive_output.nil? || directive_output.strip.empty?
|
|
524
417
|
|
|
525
|
-
|
|
526
|
-
nil
|
|
418
|
+
handle_successful_directive(follow_up_prompt, directive_output)
|
|
527
419
|
end
|
|
528
420
|
|
|
529
421
|
def handle_empty_directive_output
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# lib/aia/topic_context.rb
|
|
2
|
+
# Just thinking about the problem ...
|
|
3
|
+
# maybe a directive like //topic [topic]
|
|
4
|
+
# sets manually (when present) or dynamically when not present
|
|
5
|
+
# and //topics - will list current topics
|
|
6
|
+
# thinking about the //checkpoint and //restore directives
|
|
7
|
+
#
|
|
8
|
+
module AIA
|
|
9
|
+
class TopicContext
|
|
10
|
+
attr_reader :context_size
|
|
11
|
+
|
|
12
|
+
# Initialize topic context manager
|
|
13
|
+
# @param context_size [Integer] max allowed bytes per topic
|
|
14
|
+
def initialize(context_size = 128_000)
|
|
15
|
+
@storage = Hash.new { |h, k| h[k] = [] } # auto-initialize empty array
|
|
16
|
+
@context_size = context_size
|
|
17
|
+
@total_chars = 0
|
|
18
|
+
@mutex = Mutex.new # ensure thread safety
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Store a request/response pair under the given topic (or auto-generate one)
|
|
22
|
+
# @param request [String]
|
|
23
|
+
# @param response [String]
|
|
24
|
+
# @param topic [String, nil]
|
|
25
|
+
# @return [String] topic name used
|
|
26
|
+
def store_conversation(request, response, topic = nil)
|
|
27
|
+
raise ArgumentError, "request and response must be strings" unless request.is_a?(String) && response.is_a?(String)
|
|
28
|
+
|
|
29
|
+
topic ||= generate_topic(request)
|
|
30
|
+
size = request.bytesize + response.bytesize
|
|
31
|
+
|
|
32
|
+
@mutex.synchronize do
|
|
33
|
+
# Add the new context
|
|
34
|
+
@storage[topic] << { request:, response:, size:, time: Time.now }
|
|
35
|
+
|
|
36
|
+
# Update the global total
|
|
37
|
+
@total_chars += size
|
|
38
|
+
|
|
39
|
+
# Trim old entries if we exceeded the per-topic limit
|
|
40
|
+
trim_topic(topic)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
topic
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Return an array of contexts for the given topic
|
|
47
|
+
# @param topic [String]
|
|
48
|
+
# @return [Array<Hash>]
|
|
49
|
+
def get_conversation(topic)
|
|
50
|
+
@mutex.synchronize { @storage[topic] || [] }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# All topic names
|
|
54
|
+
# @return [Array<String>]
|
|
55
|
+
def topics
|
|
56
|
+
@mutex.synchronize { @storage.keys }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Hash of topic => array_of_contexts
|
|
60
|
+
# @return [Hash<String, Array<Hash>>]
|
|
61
|
+
def all_conversations
|
|
62
|
+
@mutex.synchronize { @storage.dup }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Total number of characters stored across all topics
|
|
66
|
+
# @return [Integer]
|
|
67
|
+
def total_chars
|
|
68
|
+
@mutex.synchronize { @total_chars }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Empty the storage and reset counters
|
|
72
|
+
def clear
|
|
73
|
+
@mutex.synchronize do
|
|
74
|
+
@storage.clear
|
|
75
|
+
@total_chars = 0
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Get memory usage statistics for a topic
|
|
80
|
+
# @param topic [String]
|
|
81
|
+
# @return [Hash{Symbol => Integer}]
|
|
82
|
+
def topic_stats(topic)
|
|
83
|
+
@mutex.synchronize do
|
|
84
|
+
return {} unless @storage.key?(topic)
|
|
85
|
+
|
|
86
|
+
{
|
|
87
|
+
count: @storage[topic].length,
|
|
88
|
+
size: topic_total_size(topic),
|
|
89
|
+
avg_size: topic_total_size(topic).fdiv(@storage[topic].length),
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
# Topic extractor with better heuristic - uses first meaningful 3 words
|
|
97
|
+
# @param request [String]
|
|
98
|
+
# @return [String]
|
|
99
|
+
def generate_topic(request)
|
|
100
|
+
cleaned = request.downcase.gsub(/[^a-z0-9\s]/, "")
|
|
101
|
+
words = cleaned.split
|
|
102
|
+
return "general" if words.empty?
|
|
103
|
+
|
|
104
|
+
words.first(3).join("_")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Remove oldest contexts from the topic until size <= @context_size
|
|
108
|
+
# @param topic [String]
|
|
109
|
+
def trim_topic(topic)
|
|
110
|
+
return unless @storage.key?(topic) && @storage[topic].size > 1
|
|
111
|
+
|
|
112
|
+
while topic_total_size(topic) > @context_size
|
|
113
|
+
removed = @storage[topic].shift # oldest context
|
|
114
|
+
@total_chars -= removed[:size] # adjust global counter
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Helper to compute the sum of sizes for a topic
|
|
119
|
+
# @param topic [String]
|
|
120
|
+
# @return [Integer]
|
|
121
|
+
def topic_total_size(topic)
|
|
122
|
+
@storage[topic].sum { |ctx| ctx[:size] }
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: aia
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.9.
|
|
4
|
+
version: 0.9.24
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -426,8 +426,8 @@ files:
|
|
|
426
426
|
- lib/aia/config/defaults.rb
|
|
427
427
|
- lib/aia/config/file_loader.rb
|
|
428
428
|
- lib/aia/config/validator.rb
|
|
429
|
-
- lib/aia/context_manager.rb
|
|
430
429
|
- lib/aia/directive_processor.rb
|
|
430
|
+
- lib/aia/directives/checkpoint.rb
|
|
431
431
|
- lib/aia/directives/configuration.rb
|
|
432
432
|
- lib/aia/directives/execution.rb
|
|
433
433
|
- lib/aia/directives/models.rb
|
|
@@ -439,6 +439,7 @@ files:
|
|
|
439
439
|
- lib/aia/prompt_handler.rb
|
|
440
440
|
- lib/aia/ruby_llm_adapter.rb
|
|
441
441
|
- lib/aia/session.rb
|
|
442
|
+
- lib/aia/topic_context.rb
|
|
442
443
|
- lib/aia/ui_presenter.rb
|
|
443
444
|
- lib/aia/utility.rb
|
|
444
445
|
- lib/aia/version.rb
|
|
@@ -477,7 +478,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
477
478
|
- !ruby/object:Gem::Version
|
|
478
479
|
version: '0'
|
|
479
480
|
requirements: []
|
|
480
|
-
rubygems_version: 4.0.
|
|
481
|
+
rubygems_version: 4.0.1
|
|
481
482
|
specification_version: 4
|
|
482
483
|
summary: Multi-model AI CLI with dynamic prompts, consensus responses, shell & Ruby
|
|
483
484
|
integration, and seamless chat workflows.
|
data/lib/aia/context_manager.rb
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
# lib/aia/context_manager.rb
|
|
2
|
-
|
|
3
|
-
module AIA
|
|
4
|
-
# Manages the conversation context for chat sessions.
|
|
5
|
-
class ContextManager
|
|
6
|
-
attr_reader :context, :checkpoints
|
|
7
|
-
|
|
8
|
-
# Initializes the ContextManager with an optional system prompt.
|
|
9
|
-
def initialize(system_prompt: nil)
|
|
10
|
-
@context = []
|
|
11
|
-
@checkpoints = {}
|
|
12
|
-
@checkpoint_counter = 0
|
|
13
|
-
add_system_prompt(system_prompt) if system_prompt && !system_prompt.strip.empty?
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# Adds a message to the conversation context.
|
|
17
|
-
#
|
|
18
|
-
# @param role [String] The role of the message sender ('user' or 'assistant').
|
|
19
|
-
# @param content [String] The content of the message.
|
|
20
|
-
def add_to_context(role:, content:)
|
|
21
|
-
@context << { role: role, content: content }
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Returns the current conversation context.
|
|
25
|
-
# Optionally adds the system prompt if it wasn't added during initialization
|
|
26
|
-
# or needs to be re-added after clearing.
|
|
27
|
-
#
|
|
28
|
-
# @param system_prompt [String, nil] The system prompt to potentially prepend.
|
|
29
|
-
# @return [Array<Hash>] The conversation context array.
|
|
30
|
-
def get_context(system_prompt: nil)
|
|
31
|
-
# Add or replace system prompt if provided and not empty
|
|
32
|
-
if system_prompt && !system_prompt.strip.empty?
|
|
33
|
-
add_system_prompt(system_prompt)
|
|
34
|
-
end
|
|
35
|
-
@context
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Clears the conversation context, optionally keeping the system prompt.
|
|
39
|
-
#
|
|
40
|
-
# @param keep_system_prompt [Boolean] Whether to retain the initial system prompt.
|
|
41
|
-
def clear_context(keep_system_prompt: true)
|
|
42
|
-
if keep_system_prompt && !@context.empty? && @context.first[:role] == 'system'
|
|
43
|
-
@context = [@context.first]
|
|
44
|
-
else
|
|
45
|
-
@context = []
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Clear all checkpoints when clearing context
|
|
49
|
-
@checkpoints.clear
|
|
50
|
-
@checkpoint_counter = 0
|
|
51
|
-
|
|
52
|
-
# Attempt to clear the LLM client's context as well
|
|
53
|
-
begin
|
|
54
|
-
if AIA.config.client && AIA.config.client.respond_to?(:clear_context)
|
|
55
|
-
AIA.config.client.clear_context
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
if AIA.config.respond_to?(:llm) && AIA.config.llm && AIA.config.llm.respond_to?(:clear_context)
|
|
59
|
-
AIA.config.llm.clear_context
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
if defined?(RubyLLM) && RubyLLM.respond_to?(:chat) && RubyLLM.chat.respond_to?(:clear_history)
|
|
63
|
-
RubyLLM.chat.clear_history
|
|
64
|
-
end
|
|
65
|
-
rescue => e
|
|
66
|
-
STDERR.puts "ERROR: context_manager clear_context error #{e.message}"
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Creates a checkpoint of the current context with an optional name.
|
|
71
|
-
#
|
|
72
|
-
# @param name [String, nil] The name of the checkpoint. If nil, uses an incrementing integer.
|
|
73
|
-
# @return [String] The name of the created checkpoint.
|
|
74
|
-
def create_checkpoint(name: nil)
|
|
75
|
-
if name.nil?
|
|
76
|
-
@checkpoint_counter += 1
|
|
77
|
-
name = @checkpoint_counter.to_s
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Store a deep copy of the current context and its position
|
|
81
|
-
@checkpoints[name] = {
|
|
82
|
-
context: @context.map(&:dup),
|
|
83
|
-
position: @context.size
|
|
84
|
-
}
|
|
85
|
-
@last_checkpoint_name = name
|
|
86
|
-
name
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# Restores the context to a previously saved checkpoint.
|
|
90
|
-
#
|
|
91
|
-
# @param name [String, nil] The name of the checkpoint to restore. If nil, uses the last checkpoint.
|
|
92
|
-
# @return [Boolean] True if restore was successful, false otherwise.
|
|
93
|
-
def restore_checkpoint(name: nil)
|
|
94
|
-
name = @last_checkpoint_name if name.nil?
|
|
95
|
-
|
|
96
|
-
return false if name.nil? || !@checkpoints.key?(name)
|
|
97
|
-
|
|
98
|
-
# Restore the context from the checkpoint
|
|
99
|
-
checkpoint_data = @checkpoints[name]
|
|
100
|
-
@context = checkpoint_data[:context].map(&:dup)
|
|
101
|
-
true
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Returns the list of available checkpoint names.
|
|
105
|
-
#
|
|
106
|
-
# @return [Array<String>] The names of all checkpoints.
|
|
107
|
-
def checkpoint_names
|
|
108
|
-
@checkpoints.keys
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Returns checkpoint information mapped to context positions.
|
|
112
|
-
#
|
|
113
|
-
# @return [Hash<Integer, Array<String>>] Position to checkpoint names mapping.
|
|
114
|
-
def checkpoint_positions
|
|
115
|
-
positions = {}
|
|
116
|
-
@checkpoints.each do |name, data|
|
|
117
|
-
position = data[:position]
|
|
118
|
-
positions[position] ||= []
|
|
119
|
-
positions[position] << name
|
|
120
|
-
end
|
|
121
|
-
positions
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
private
|
|
125
|
-
|
|
126
|
-
# Adds or replaces the system prompt at the beginning of the context.
|
|
127
|
-
def add_system_prompt(system_prompt)
|
|
128
|
-
# Remove existing system prompt if present
|
|
129
|
-
@context.shift if !@context.empty? && @context.first[:role] == 'system'
|
|
130
|
-
# Add the new system prompt at the beginning
|
|
131
|
-
@context.unshift({ role: 'system', content: system_prompt })
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
end
|