askcii 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/CLAUDE.md +131 -0
- data/Gemfile.lock +7 -3
- data/README.md +348 -109
- data/Rakefile +19 -3
- data/askcii.gemspec +4 -2
- data/completions/README.md +64 -0
- data/completions/askcii.bash +29 -0
- data/completions/askcii.zsh +27 -0
- data/lib/askcii/application.rb +80 -31
- data/lib/askcii/chat_factory.rb +80 -0
- data/lib/askcii/chat_session.rb +71 -44
- data/lib/askcii/cli.rb +49 -3
- data/lib/askcii/commands/base_command.rb +45 -0
- data/lib/askcii/commands/chat_command.rb +56 -0
- data/lib/askcii/commands/clear_history_command.rb +31 -0
- data/lib/askcii/commands/configure_command.rb +17 -0
- data/lib/askcii/commands/help_command.rb +52 -0
- data/lib/askcii/commands/history_command.rb +46 -0
- data/lib/askcii/commands/last_response_command.rb +31 -0
- data/lib/askcii/commands/list_sessions_command.rb +36 -0
- data/lib/askcii/config_validator.rb +90 -0
- data/lib/askcii/configuration_manager.rb +166 -54
- data/lib/askcii/errors.rb +15 -0
- data/lib/askcii/models/chat.rb +6 -3
- data/lib/askcii/models/config.rb +12 -4
- data/lib/askcii/provider_config.rb +116 -0
- data/lib/askcii/version.rb +1 -1
- data/lib/askcii.rb +72 -34
- metadata +48 -4
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Askcii
|
|
4
|
+
module Commands
|
|
5
|
+
# Clears conversation history for the current session
|
|
6
|
+
class ClearHistoryCommand < BaseCommand
|
|
7
|
+
def execute
|
|
8
|
+
verbose "Clearing history for session: #{session_context}"
|
|
9
|
+
|
|
10
|
+
chat = Chat.find(context: session_context)
|
|
11
|
+
|
|
12
|
+
unless chat
|
|
13
|
+
puts "No session found to clear."
|
|
14
|
+
exit 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
message_count = chat.messages.count
|
|
18
|
+
|
|
19
|
+
# Delete all messages for this chat
|
|
20
|
+
chat.messages_dataset.delete
|
|
21
|
+
|
|
22
|
+
# Delete the chat itself
|
|
23
|
+
chat.delete
|
|
24
|
+
|
|
25
|
+
puts "Cleared session '#{session_context}' (#{message_count} messages removed)"
|
|
26
|
+
|
|
27
|
+
exit 0
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Askcii
|
|
4
|
+
module Commands
|
|
5
|
+
# Opens configuration management interface
|
|
6
|
+
class ConfigureCommand < BaseCommand
|
|
7
|
+
def execute
|
|
8
|
+
verbose "Opening configuration manager..."
|
|
9
|
+
|
|
10
|
+
require_relative '../configuration_manager'
|
|
11
|
+
ConfigurationManager.new.run
|
|
12
|
+
|
|
13
|
+
exit 0
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Askcii
|
|
4
|
+
module Commands
|
|
5
|
+
# Displays help information
|
|
6
|
+
class HelpCommand < BaseCommand
|
|
7
|
+
def execute
|
|
8
|
+
puts <<~HELP
|
|
9
|
+
askcii - Command-line interface for multiple LLM providers
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
askcii [options] "Your prompt here"
|
|
13
|
+
echo "input" | askcii [options] "Your prompt"
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
-p, --private Create a private session (no history saved)
|
|
17
|
+
-r, --last-response Retrieve the last assistant response from current session
|
|
18
|
+
-c, --configure Open configuration management interface
|
|
19
|
+
-m, --model ID Use specific configuration by ID
|
|
20
|
+
-v, --verbose Show detailed information during execution
|
|
21
|
+
--session NAME Use specific session name (alternative to ASKCII_SESSION)
|
|
22
|
+
--list-sessions List all available sessions
|
|
23
|
+
--history Show current session history
|
|
24
|
+
--clear-history Clear current session history
|
|
25
|
+
-h, --help Show this help message
|
|
26
|
+
|
|
27
|
+
Environment Variables:
|
|
28
|
+
ASKCII_SESSION Session identifier for conversation persistence
|
|
29
|
+
ASKCII_API_KEY API key (legacy, use -c to configure)
|
|
30
|
+
ASKCII_API_ENDPOINT API endpoint (legacy)
|
|
31
|
+
ASKCII_MODEL_ID Model ID (legacy)
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
askcii "Explain what a Ruby block is"
|
|
35
|
+
askcii -p "What is the capital of France?"
|
|
36
|
+
echo "def hello\nputs 'hi'\nend" | askcii "Explain this Ruby code"
|
|
37
|
+
askcii -m 2 "Use configuration #2"
|
|
38
|
+
askcii --session my-project "Continue conversation in my-project session"
|
|
39
|
+
ASKCII_SESSION=work askcii "Use work session"
|
|
40
|
+
|
|
41
|
+
Configuration:
|
|
42
|
+
Run 'askcii -c' to configure providers interactively.
|
|
43
|
+
Supports: OpenAI, Anthropic, Gemini, DeepSeek, OpenRouter, Ollama
|
|
44
|
+
|
|
45
|
+
For more information, visit: https://github.com/yourusername/askcii
|
|
46
|
+
HELP
|
|
47
|
+
|
|
48
|
+
exit 0
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Askcii
|
|
4
|
+
module Commands
|
|
5
|
+
# Displays conversation history for the current session
|
|
6
|
+
class HistoryCommand < BaseCommand
|
|
7
|
+
def execute
|
|
8
|
+
verbose "Retrieving history for session: #{session_context}"
|
|
9
|
+
|
|
10
|
+
chat = Chat.find(context: session_context)
|
|
11
|
+
|
|
12
|
+
unless chat
|
|
13
|
+
puts "No history found for this session."
|
|
14
|
+
exit 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
messages = chat.messages_dataset.order(:created_at).all
|
|
18
|
+
|
|
19
|
+
if messages.empty?
|
|
20
|
+
puts "No messages in this session."
|
|
21
|
+
exit 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
puts "Session: #{chat.context}"
|
|
25
|
+
puts "Model: #{chat.model_id}"
|
|
26
|
+
puts "Messages: #{messages.count}"
|
|
27
|
+
puts
|
|
28
|
+
puts "=" * 80
|
|
29
|
+
puts
|
|
30
|
+
|
|
31
|
+
messages.each do |msg|
|
|
32
|
+
role_label = msg.role.to_s.capitalize
|
|
33
|
+
timestamp = msg.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
|
34
|
+
|
|
35
|
+
puts "#{role_label} (#{timestamp}):"
|
|
36
|
+
puts msg.content
|
|
37
|
+
puts
|
|
38
|
+
puts "-" * 80
|
|
39
|
+
puts
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
exit 0
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Askcii
|
|
4
|
+
module Commands
|
|
5
|
+
# Retrieves and displays the last assistant response from the current session
|
|
6
|
+
class LastResponseCommand < BaseCommand
|
|
7
|
+
def execute
|
|
8
|
+
verbose "Retrieving last response from session: #{session_context}"
|
|
9
|
+
|
|
10
|
+
chat = Chat.find(context: session_context)
|
|
11
|
+
|
|
12
|
+
unless chat
|
|
13
|
+
error! "No chat session found for context: #{session_context}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
last_message = chat.messages_dataset
|
|
17
|
+
.where(role: 'assistant')
|
|
18
|
+
.order(Sequel.desc(:created_at))
|
|
19
|
+
.first
|
|
20
|
+
|
|
21
|
+
unless last_message
|
|
22
|
+
error! "No assistant messages found in this session"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
puts last_message.content
|
|
26
|
+
|
|
27
|
+
exit 0
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Askcii
|
|
4
|
+
module Commands
|
|
5
|
+
# Lists all available chat sessions
|
|
6
|
+
class ListSessionsCommand < BaseCommand
|
|
7
|
+
def execute
|
|
8
|
+
verbose "Listing all sessions..."
|
|
9
|
+
|
|
10
|
+
chats = Chat.order(Sequel.desc(:created_at)).all
|
|
11
|
+
|
|
12
|
+
if chats.empty?
|
|
13
|
+
puts "No sessions found."
|
|
14
|
+
exit 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
puts "Available sessions:"
|
|
18
|
+
puts
|
|
19
|
+
|
|
20
|
+
chats.each do |chat|
|
|
21
|
+
message_count = chat.messages.count
|
|
22
|
+
last_message = chat.messages_dataset.order(Sequel.desc(:created_at)).first
|
|
23
|
+
last_updated = last_message&.created_at || chat.created_at
|
|
24
|
+
|
|
25
|
+
puts "Session: #{chat.context}"
|
|
26
|
+
puts " Model: #{chat.model_id}"
|
|
27
|
+
puts " Messages: #{message_count}"
|
|
28
|
+
puts " Last updated: #{last_updated}"
|
|
29
|
+
puts
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
exit 0
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Askcii
|
|
4
|
+
# Validates configuration settings before use
|
|
5
|
+
class ConfigValidator
|
|
6
|
+
VALID_PROVIDERS = %w[openai anthropic gemini deepseek openrouter ollama].freeze
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
# Validates a configuration hash
|
|
10
|
+
# @param config [Hash] Configuration to validate
|
|
11
|
+
# @raise [ConfigurationError] if configuration is invalid
|
|
12
|
+
# @return [Hash] The validated configuration
|
|
13
|
+
def validate!(config)
|
|
14
|
+
validate_presence!(config)
|
|
15
|
+
validate_provider!(config)
|
|
16
|
+
validate_api_key!(config)
|
|
17
|
+
validate_model_id!(config)
|
|
18
|
+
validate_api_endpoint!(config)
|
|
19
|
+
config
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Checks if a provider name is valid
|
|
23
|
+
# @param name [String] Provider name
|
|
24
|
+
# @return [Boolean]
|
|
25
|
+
def valid_provider?(name)
|
|
26
|
+
VALID_PROVIDERS.include?(name.to_s.downcase)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Lists all valid provider names
|
|
30
|
+
# @return [Array<String>]
|
|
31
|
+
def valid_providers
|
|
32
|
+
VALID_PROVIDERS
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def validate_presence!(config)
|
|
38
|
+
raise ConfigurationError, "Configuration cannot be nil" if config.nil?
|
|
39
|
+
raise ConfigurationError, "Configuration cannot be empty" if config.empty?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def validate_provider!(config)
|
|
43
|
+
provider = config[:provider].to_s.strip
|
|
44
|
+
if provider.empty?
|
|
45
|
+
raise ConfigurationError, "Provider is required"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
unless valid_provider?(provider)
|
|
49
|
+
raise ConfigurationError,
|
|
50
|
+
"Invalid provider '#{provider}'. Must be one of: #{VALID_PROVIDERS.join(', ')}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def validate_api_key!(config)
|
|
55
|
+
provider = config[:provider].to_s.downcase
|
|
56
|
+
|
|
57
|
+
# Ollama doesn't require an API key
|
|
58
|
+
return if provider == 'ollama'
|
|
59
|
+
|
|
60
|
+
api_key = config[:api_key].to_s.strip
|
|
61
|
+
if api_key.empty?
|
|
62
|
+
raise ConfigurationError, "API key is required for #{provider}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if api_key == 'blank'
|
|
66
|
+
raise ConfigurationError, "API key cannot be 'blank'"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def validate_model_id!(config)
|
|
71
|
+
model_id = config[:model_id].to_s.strip
|
|
72
|
+
if model_id.empty?
|
|
73
|
+
raise ConfigurationError, "Model ID is required"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def validate_api_endpoint!(config)
|
|
78
|
+
endpoint = config[:api_endpoint].to_s.strip
|
|
79
|
+
if endpoint.empty?
|
|
80
|
+
raise ConfigurationError, "API endpoint is required"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Basic URL format validation
|
|
84
|
+
unless endpoint.start_with?('http://', 'https://')
|
|
85
|
+
raise ConfigurationError, "API endpoint must start with http:// or https://"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Askcii
|
|
4
|
+
# Interactive terminal UI for managing provider configurations
|
|
4
5
|
class ConfigurationManager
|
|
5
|
-
|
|
6
|
+
# Maps user selection numbers to provider identifiers
|
|
7
|
+
PROVIDER_MENU = {
|
|
6
8
|
'1' => 'openai',
|
|
7
9
|
'2' => 'anthropic',
|
|
8
10
|
'3' => 'gemini',
|
|
@@ -11,41 +13,50 @@ module Askcii
|
|
|
11
13
|
'6' => 'ollama'
|
|
12
14
|
}.freeze
|
|
13
15
|
|
|
14
|
-
DEFAULT_ENDPOINTS = {
|
|
15
|
-
'openai' => 'https://api.openai.com/v1',
|
|
16
|
-
'anthropic' => 'https://api.anthropic.com',
|
|
17
|
-
'gemini' => 'https://generativelanguage.googleapis.com/v1',
|
|
18
|
-
'deepseek' => 'https://api.deepseek.com/v1',
|
|
19
|
-
'openrouter' => 'https://openrouter.ai/api/v1',
|
|
20
|
-
'ollama' => 'http://localhost:11434/v1'
|
|
21
|
-
}.freeze
|
|
22
|
-
|
|
23
16
|
def run
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
loop do
|
|
18
|
+
show_current_configurations
|
|
19
|
+
show_menu
|
|
20
|
+
choice = handle_user_choice
|
|
21
|
+
break if choice == :exit
|
|
22
|
+
end
|
|
27
23
|
end
|
|
28
24
|
|
|
29
25
|
private
|
|
30
26
|
|
|
31
27
|
def show_current_configurations
|
|
28
|
+
puts
|
|
32
29
|
puts 'Configuration Management'
|
|
33
|
-
puts '
|
|
30
|
+
puts '=' * 50
|
|
31
|
+
puts
|
|
34
32
|
|
|
35
33
|
configs = Askcii::Config.configurations
|
|
36
34
|
default_id = Askcii::Config.default_configuration_id
|
|
37
35
|
|
|
38
36
|
if configs.empty?
|
|
39
37
|
puts 'No configurations found.'
|
|
38
|
+
puts 'Add a configuration to get started.'
|
|
40
39
|
else
|
|
41
40
|
puts 'Current configurations:'
|
|
41
|
+
puts
|
|
42
42
|
configs.each do |config|
|
|
43
|
-
|
|
44
|
-
provider_info = config['provider'] ? " [#{config['provider']}]" : ''
|
|
45
|
-
puts " #{config['id']}. #{config['name']}#{provider_info}#{marker}"
|
|
43
|
+
show_configuration_summary(config, default_id)
|
|
46
44
|
end
|
|
47
|
-
puts
|
|
48
45
|
end
|
|
46
|
+
puts
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def show_configuration_summary(config, default_id)
|
|
50
|
+
is_default = config['id'] == default_id
|
|
51
|
+
marker = is_default ? ' ★ (default)' : ''
|
|
52
|
+
provider_name = ProviderConfig.display_name(config['provider'])
|
|
53
|
+
|
|
54
|
+
puts " #{config['id']}. #{config['name']}"
|
|
55
|
+
puts " Provider: #{provider_name}"
|
|
56
|
+
puts " Model: #{config['model_id']}"
|
|
57
|
+
puts " Endpoint: #{config['api_endpoint']}"
|
|
58
|
+
puts "#{marker}"
|
|
59
|
+
puts
|
|
49
60
|
end
|
|
50
61
|
|
|
51
62
|
def show_menu
|
|
@@ -53,8 +64,9 @@ module Askcii
|
|
|
53
64
|
puts ' 1. Add new configuration'
|
|
54
65
|
puts ' 2. Set default configuration'
|
|
55
66
|
puts ' 3. Delete configuration'
|
|
56
|
-
puts ' 4.
|
|
57
|
-
|
|
67
|
+
puts ' 4. Test configuration'
|
|
68
|
+
puts ' 5. Exit'
|
|
69
|
+
print 'Select option (1-5): '
|
|
58
70
|
end
|
|
59
71
|
|
|
60
72
|
def handle_user_choice
|
|
@@ -63,20 +75,32 @@ module Askcii
|
|
|
63
75
|
case choice
|
|
64
76
|
when '1'
|
|
65
77
|
add_new_configuration
|
|
78
|
+
:continue
|
|
66
79
|
when '2'
|
|
67
80
|
set_default_configuration
|
|
81
|
+
:continue
|
|
68
82
|
when '3'
|
|
69
83
|
delete_configuration
|
|
84
|
+
:continue
|
|
70
85
|
when '4'
|
|
71
|
-
|
|
86
|
+
test_configuration
|
|
87
|
+
:continue
|
|
88
|
+
when '5'
|
|
89
|
+
puts 'Exiting configuration manager.'
|
|
90
|
+
:exit
|
|
72
91
|
else
|
|
73
|
-
puts 'Invalid option.'
|
|
92
|
+
puts 'Invalid option. Please select 1-5.'
|
|
93
|
+
:continue
|
|
74
94
|
end
|
|
75
95
|
end
|
|
76
96
|
|
|
77
97
|
def add_new_configuration
|
|
98
|
+
puts
|
|
99
|
+
puts 'Add New Configuration'
|
|
100
|
+
puts '-' * 50
|
|
101
|
+
|
|
78
102
|
print 'Enter configuration name: '
|
|
79
|
-
name = $stdin.gets.chomp
|
|
103
|
+
name = $stdin.gets.chomp.strip
|
|
80
104
|
|
|
81
105
|
provider = select_provider
|
|
82
106
|
return unless provider
|
|
@@ -85,29 +109,66 @@ module Askcii
|
|
|
85
109
|
return unless api_key || provider == 'ollama'
|
|
86
110
|
|
|
87
111
|
endpoint = get_api_endpoint(provider)
|
|
88
|
-
model_id = get_model_id
|
|
89
|
-
|
|
112
|
+
model_id = get_model_id(provider)
|
|
90
113
|
return unless model_id
|
|
91
114
|
|
|
115
|
+
# Use model_id as name if no name provided
|
|
92
116
|
name = model_id if name.empty?
|
|
93
|
-
|
|
94
|
-
|
|
117
|
+
|
|
118
|
+
# Build configuration hash
|
|
119
|
+
config = {
|
|
120
|
+
name: name,
|
|
121
|
+
api_key: api_key || '',
|
|
122
|
+
api_endpoint: endpoint,
|
|
123
|
+
model_id: model_id,
|
|
124
|
+
provider: provider
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Validate configuration
|
|
128
|
+
begin
|
|
129
|
+
ConfigValidator.validate!(config)
|
|
130
|
+
rescue ConfigurationError => e
|
|
131
|
+
puts "Validation failed: #{e.message}"
|
|
132
|
+
return
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Save configuration
|
|
136
|
+
Askcii::Config.add_configuration(
|
|
137
|
+
name: name,
|
|
138
|
+
api_key: api_key || '',
|
|
139
|
+
api_endpoint: endpoint,
|
|
140
|
+
model_id: model_id,
|
|
141
|
+
provider: provider
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
puts
|
|
145
|
+
puts '✓ Configuration added successfully!'
|
|
146
|
+
puts
|
|
147
|
+
|
|
148
|
+
# Offer to test the configuration
|
|
149
|
+
print 'Test this configuration now? (y/n): '
|
|
150
|
+
if $stdin.gets.chomp.downcase == 'y'
|
|
151
|
+
# Get the newly added configuration ID
|
|
152
|
+
configs = Askcii::Config.configurations
|
|
153
|
+
new_config = configs.last
|
|
154
|
+
test_specific_configuration(new_config) if new_config
|
|
155
|
+
end
|
|
95
156
|
end
|
|
96
157
|
|
|
97
158
|
def select_provider
|
|
159
|
+
puts
|
|
98
160
|
puts 'Select provider:'
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if provider.nil?
|
|
161
|
+
ProviderConfig.list.each_with_index do |provider_id, index|
|
|
162
|
+
provider_meta = ProviderConfig.get(provider_id)
|
|
163
|
+
key_info = provider_meta[:requires_key] ? '' : ' (no API key needed)'
|
|
164
|
+
puts " #{index + 1}. #{provider_meta[:name]}#{key_info}"
|
|
165
|
+
end
|
|
166
|
+
print "Provider (1-#{ProviderConfig.list.size}): "
|
|
167
|
+
|
|
168
|
+
choice = $stdin.gets.chomp
|
|
169
|
+
provider = PROVIDER_MENU[choice]
|
|
170
|
+
|
|
171
|
+
if provider.nil? || !ProviderConfig.valid?(provider)
|
|
111
172
|
puts 'Invalid provider selection.'
|
|
112
173
|
return nil
|
|
113
174
|
end
|
|
@@ -118,11 +179,12 @@ module Askcii
|
|
|
118
179
|
def get_api_key(provider)
|
|
119
180
|
return '' if provider == 'ollama'
|
|
120
181
|
|
|
121
|
-
|
|
122
|
-
|
|
182
|
+
provider_name = ProviderConfig.display_name(provider)
|
|
183
|
+
print "Enter #{provider_name} API key: "
|
|
184
|
+
api_key = $stdin.gets.chomp.strip
|
|
123
185
|
|
|
124
|
-
if api_key.empty?
|
|
125
|
-
puts
|
|
186
|
+
if api_key.empty? && ProviderConfig.requires_key?(provider)
|
|
187
|
+
puts "API key is required for #{provider_name}."
|
|
126
188
|
return nil
|
|
127
189
|
end
|
|
128
190
|
|
|
@@ -130,15 +192,17 @@ module Askcii
|
|
|
130
192
|
end
|
|
131
193
|
|
|
132
194
|
def get_api_endpoint(provider)
|
|
133
|
-
default_endpoint =
|
|
195
|
+
default_endpoint = ProviderConfig.default_endpoint(provider)
|
|
134
196
|
print "Enter API endpoint (default: #{default_endpoint}): "
|
|
135
|
-
api_endpoint = $stdin.gets.chomp
|
|
197
|
+
api_endpoint = $stdin.gets.chomp.strip
|
|
136
198
|
api_endpoint.empty? ? default_endpoint : api_endpoint
|
|
137
199
|
end
|
|
138
200
|
|
|
139
|
-
def get_model_id
|
|
201
|
+
def get_model_id(provider)
|
|
202
|
+
examples = ProviderConfig.example_models(provider)
|
|
203
|
+
puts "Example models for #{ProviderConfig.display_name(provider)}: #{examples.join(', ')}"
|
|
140
204
|
print 'Enter model ID: '
|
|
141
|
-
model_id = $stdin.gets.chomp
|
|
205
|
+
model_id = $stdin.gets.chomp.strip
|
|
142
206
|
|
|
143
207
|
if model_id.empty?
|
|
144
208
|
puts 'Model ID is required.'
|
|
@@ -156,14 +220,15 @@ module Askcii
|
|
|
156
220
|
return
|
|
157
221
|
end
|
|
158
222
|
|
|
223
|
+
puts
|
|
159
224
|
print 'Enter configuration ID to set as default: '
|
|
160
|
-
new_default = $stdin.gets.chomp
|
|
225
|
+
new_default = $stdin.gets.chomp.strip
|
|
161
226
|
|
|
162
227
|
if configs.any? { |c| c['id'] == new_default }
|
|
163
228
|
Askcii::Config.set_default_configuration(new_default)
|
|
164
|
-
puts "Configuration #{new_default} set as default."
|
|
229
|
+
puts "✓ Configuration #{new_default} set as default."
|
|
165
230
|
else
|
|
166
|
-
puts 'Invalid configuration ID.'
|
|
231
|
+
puts '✗ Invalid configuration ID.'
|
|
167
232
|
end
|
|
168
233
|
end
|
|
169
234
|
|
|
@@ -175,18 +240,65 @@ module Askcii
|
|
|
175
240
|
return
|
|
176
241
|
end
|
|
177
242
|
|
|
243
|
+
puts
|
|
178
244
|
print 'Enter configuration ID to delete: '
|
|
179
|
-
delete_id = $stdin.gets.chomp
|
|
245
|
+
delete_id = $stdin.gets.chomp.strip
|
|
180
246
|
|
|
181
247
|
if configs.any? { |c| c['id'] == delete_id }
|
|
248
|
+
print "Are you sure you want to delete configuration #{delete_id}? (y/n): "
|
|
249
|
+
return unless $stdin.gets.chomp.downcase == 'y'
|
|
250
|
+
|
|
182
251
|
if Askcii::Config.delete_configuration(delete_id)
|
|
183
|
-
puts "Configuration #{delete_id} deleted successfully."
|
|
252
|
+
puts "✓ Configuration #{delete_id} deleted successfully."
|
|
184
253
|
else
|
|
185
|
-
puts 'Failed to delete configuration.'
|
|
254
|
+
puts '✗ Failed to delete configuration.'
|
|
186
255
|
end
|
|
187
256
|
else
|
|
188
|
-
puts 'Invalid configuration ID.'
|
|
257
|
+
puts '✗ Invalid configuration ID.'
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def test_configuration
|
|
262
|
+
configs = Askcii::Config.configurations
|
|
263
|
+
|
|
264
|
+
if configs.empty?
|
|
265
|
+
puts 'No configurations available to test.'
|
|
266
|
+
return
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
puts
|
|
270
|
+
print 'Enter configuration ID to test: '
|
|
271
|
+
config_id = $stdin.gets.chomp.strip
|
|
272
|
+
|
|
273
|
+
config = configs.find { |c| c['id'] == config_id }
|
|
274
|
+
|
|
275
|
+
if config
|
|
276
|
+
test_specific_configuration(config)
|
|
277
|
+
else
|
|
278
|
+
puts '✗ Invalid configuration ID.'
|
|
189
279
|
end
|
|
190
280
|
end
|
|
281
|
+
|
|
282
|
+
def test_specific_configuration(config)
|
|
283
|
+
puts
|
|
284
|
+
puts "Testing configuration: #{config['name']}"
|
|
285
|
+
puts '-' * 50
|
|
286
|
+
|
|
287
|
+
# Validate first
|
|
288
|
+
begin
|
|
289
|
+
symbolized_config = config.transform_keys(&:to_sym)
|
|
290
|
+
ConfigValidator.validate!(symbolized_config)
|
|
291
|
+
puts '✓ Configuration validation passed'
|
|
292
|
+
rescue ConfigurationError => e
|
|
293
|
+
puts "✗ Validation failed: #{e.message}"
|
|
294
|
+
return
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# TODO: Add actual connectivity test with minimal LLM call
|
|
298
|
+
# For now, just validate the configuration structure
|
|
299
|
+
puts '✓ Configuration appears valid'
|
|
300
|
+
puts
|
|
301
|
+
puts 'Note: Actual connectivity testing will be added in a future update.'
|
|
302
|
+
end
|
|
191
303
|
end
|
|
192
304
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Askcii
|
|
4
|
+
# Base error class for all Askcii errors
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when configuration is invalid or missing
|
|
8
|
+
class ConfigurationError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when session operations fail
|
|
11
|
+
class SessionError < Error; end
|
|
12
|
+
|
|
13
|
+
# Raised when validation fails
|
|
14
|
+
class ValidationError < Error; end
|
|
15
|
+
end
|
data/lib/askcii/models/chat.rb
CHANGED
|
@@ -4,9 +4,12 @@ module Askcii
|
|
|
4
4
|
class Chat < Sequel::Model(Askcii.database[:chats])
|
|
5
5
|
one_to_many :messages, class: 'Askcii::Message', key: :chat_id
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
# Converts this Chat to a RubyLLM::Chat object with message history
|
|
8
|
+
# @param config [Hash] Configuration hash with provider and model settings
|
|
9
|
+
# @return [RubyLLM::Chat]
|
|
10
|
+
def to_llm(config)
|
|
11
|
+
provider = config[:provider] || config['provider'] || 'openai'
|
|
12
|
+
provider_symbol = provider.to_sym
|
|
10
13
|
|
|
11
14
|
@chat = RubyLLM.chat(
|
|
12
15
|
model: model_id,
|