llm_gateway 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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/README.md +565 -129
  4. data/Rakefile +8 -3
  5. data/docs/migration-guide.md +135 -0
  6. data/lib/llm_gateway/adapters/adapter.rb +173 -0
  7. data/lib/llm_gateway/adapters/anthropic/acts_like_messages.rb +23 -0
  8. data/lib/llm_gateway/adapters/anthropic/bidirectional_message_mapper.rb +111 -0
  9. data/lib/llm_gateway/adapters/{claude → anthropic}/input_mapper.rb +12 -10
  10. data/lib/llm_gateway/adapters/anthropic/messages_adapter.rb +19 -0
  11. data/lib/llm_gateway/adapters/anthropic/output_mapper.rb +50 -0
  12. data/lib/llm_gateway/adapters/anthropic/stream_mapper.rb +110 -0
  13. data/lib/llm_gateway/adapters/anthropic_option_mapper.rb +53 -0
  14. data/lib/llm_gateway/adapters/groq/chat_completions_adapter.rb +47 -0
  15. data/lib/llm_gateway/adapters/groq/option_mapper.rb +27 -0
  16. data/lib/llm_gateway/adapters/input_message_sanitizer.rb +93 -0
  17. data/lib/llm_gateway/adapters/openai/acts_like_chat_completions.rb +22 -0
  18. data/lib/llm_gateway/adapters/openai/acts_like_responses.rb +31 -0
  19. data/lib/llm_gateway/adapters/openai/chat_completions/bidirectional_message_mapper.rb +110 -0
  20. data/lib/llm_gateway/adapters/openai/chat_completions/input_mapper.rb +105 -0
  21. data/lib/llm_gateway/adapters/openai/chat_completions/input_message_sanitizer.rb +65 -0
  22. data/lib/llm_gateway/adapters/openai/chat_completions/option_mapper.rb +39 -0
  23. data/lib/llm_gateway/adapters/openai/chat_completions/output_mapper.rb +40 -0
  24. data/lib/llm_gateway/adapters/openai/chat_completions/stream_mapper.rb +242 -0
  25. data/lib/llm_gateway/adapters/openai/chat_completions_adapter.rb +20 -0
  26. data/lib/llm_gateway/adapters/openai/file_output_mapper.rb +25 -0
  27. data/lib/llm_gateway/adapters/openai/prompt_cache_option_mapper.rb +39 -0
  28. data/lib/llm_gateway/adapters/openai/responses/bidirectional_message_mapper.rb +120 -0
  29. data/lib/llm_gateway/adapters/openai/responses/input_mapper.rb +106 -0
  30. data/lib/llm_gateway/adapters/openai/responses/option_mapper.rb +41 -0
  31. data/lib/llm_gateway/adapters/openai/responses/output_mapper.rb +47 -0
  32. data/lib/llm_gateway/adapters/openai/responses/stream_mapper.rb +340 -0
  33. data/lib/llm_gateway/adapters/openai/responses_adapter.rb +20 -0
  34. data/lib/llm_gateway/adapters/openai_codex/input_mapper.rb +206 -0
  35. data/lib/llm_gateway/adapters/openai_codex/option_mapper.rb +28 -0
  36. data/lib/llm_gateway/adapters/openai_codex/responses_adapter.rb +38 -0
  37. data/lib/llm_gateway/adapters/{open_ai/output_mapper.rb → option_mapper.rb} +5 -2
  38. data/lib/llm_gateway/adapters/stream_accumulator.rb +91 -0
  39. data/lib/llm_gateway/adapters/structs.rb +145 -0
  40. data/lib/llm_gateway/base_client.rb +97 -1
  41. data/lib/llm_gateway/client.rb +66 -54
  42. data/lib/llm_gateway/clients/anthropic.rb +167 -0
  43. data/lib/llm_gateway/clients/claude_code/oauth_flow.rb +162 -0
  44. data/lib/llm_gateway/clients/claude_code/token_manager.rb +112 -0
  45. data/lib/llm_gateway/clients/groq.rb +54 -0
  46. data/lib/llm_gateway/clients/openai.rb +208 -0
  47. data/lib/llm_gateway/clients/openai_codex/oauth_flow.rb +258 -0
  48. data/lib/llm_gateway/clients/openai_codex/token_manager.rb +71 -0
  49. data/lib/llm_gateway/errors.rb +23 -0
  50. data/lib/llm_gateway/prompt.rb +12 -1
  51. data/lib/llm_gateway/provider_registry.rb +37 -0
  52. data/lib/llm_gateway/version.rb +1 -1
  53. data/lib/llm_gateway.rb +169 -10
  54. data/scripts/create_anthropic_credentials.rb +106 -0
  55. data/scripts/create_openai_codex_credentials.rb +116 -0
  56. data/scripts/generate_handoff_live_fixture.rb +169 -0
  57. data/scripts/generate_handoff_media_fixture.rb +167 -0
  58. metadata +64 -21
  59. data/lib/llm_gateway/adapters/claude/client.rb +0 -56
  60. data/lib/llm_gateway/adapters/claude/output_mapper.rb +0 -30
  61. data/lib/llm_gateway/adapters/groq/client.rb +0 -58
  62. data/lib/llm_gateway/adapters/groq/input_mapper.rb +0 -105
  63. data/lib/llm_gateway/adapters/groq/output_mapper.rb +0 -62
  64. data/lib/llm_gateway/adapters/open_ai/client.rb +0 -59
  65. data/lib/llm_gateway/adapters/open_ai/input_mapper.rb +0 -63
  66. data/sample/claude_code_clone/agent.rb +0 -65
  67. data/sample/claude_code_clone/claude_code_clone.rb +0 -40
  68. data/sample/claude_code_clone/prompt.rb +0 -79
  69. data/sample/claude_code_clone/run.rb +0 -47
  70. data/sample/claude_code_clone/tools/bash_tool.rb +0 -54
  71. data/sample/claude_code_clone/tools/edit_tool.rb +0 -61
  72. data/sample/claude_code_clone/tools/grep_tool.rb +0 -113
  73. data/sample/claude_code_clone/tools/read_tool.rb +0 -61
  74. data/sample/claude_code_clone/tools/todowrite_tool.rb +0 -98
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../../base_client"
4
-
5
- module LlmGateway
6
- module Adapters
7
- module Claude
8
- class Client < BaseClient
9
- def initialize(model_key: "claude-3-7-sonnet-20250219", api_key: ENV["ANTHROPIC_API_KEY"])
10
- @base_endpoint = "https://api.anthropic.com/v1"
11
- super(model_key: model_key, api_key: api_key)
12
- end
13
-
14
- def chat(messages, response_format: { type: "text" }, tools: nil, system: [], max_completion_tokens: 4096)
15
- body = {
16
- model: model_key,
17
- max_tokens: max_completion_tokens,
18
- messages: messages
19
- }
20
-
21
- body.merge!(tools: tools) if LlmGateway::Utils.present?(tools)
22
- body.merge!(system: system) if LlmGateway::Utils.present?(system)
23
-
24
- post("messages", body)
25
- end
26
-
27
- def download_file(file_id)
28
- get("files/#{file_id}/content")
29
- end
30
-
31
- private
32
-
33
- def build_headers
34
- {
35
- "anthropic-version" => "2023-06-01",
36
- "content-type" => "application/json",
37
- "x-api-key" => api_key,
38
- "anthropic-beta" => "code-execution-2025-05-22,files-api-2025-04-14"
39
- }
40
- end
41
-
42
- def handle_client_specific_errors(response, error)
43
- case response.code.to_i
44
- when 400
45
- if error["message"]&.start_with?("prompt is too long")
46
- raise Errors::PromptTooLong.new(error["message"], error["type"])
47
- end
48
- end
49
-
50
- # If we get here, we didn't handle it specifically
51
- raise Errors::APIStatusError.new(error["message"], error["type"])
52
- end
53
- end
54
- end
55
- end
56
- end
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmGateway
4
- module Adapters
5
- module Claude
6
- class OutputMapper
7
- def self.map(data)
8
- {
9
- id: data[:id],
10
- model: data[:model],
11
- usage: data[:usage],
12
- choices: map_choices(data)
13
- }
14
- end
15
-
16
- private
17
-
18
- def self.map_choices(data)
19
- # Claude returns content directly at root level, not in a choices array
20
- # We need to construct the choices array from the full response data
21
- [ {
22
- content: data[:content] || [], # Use content directly from Claude response
23
- finish_reason: data[:stop_reason],
24
- role: "assistant"
25
- } ]
26
- end
27
- end
28
- end
29
- end
30
- end
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../../base_client"
4
-
5
- module LlmGateway
6
- module Adapters
7
- module Groq
8
- class Client < BaseClient
9
- def initialize(model_key: "gemma2-9b-it", api_key: ENV["GROQ_API_KEY"])
10
- @base_endpoint = "https://api.groq.com/openai/v1"
11
- super(model_key: model_key, api_key: api_key)
12
- end
13
-
14
- def chat(messages, response_format: { type: "text" }, tools: nil, system: [], max_completion_tokens: 4096)
15
- body = {
16
- model: model_key,
17
- messages: system + messages,
18
- temperature: 0,
19
- max_completion_tokens: max_completion_tokens,
20
- response_format: response_format,
21
- tools: tools
22
- }
23
-
24
- post("chat/completions", body)
25
- end
26
-
27
- private
28
-
29
- def build_headers
30
- {
31
- "content-type" => "application/json",
32
- "Authorization" => "Bearer #{api_key}"
33
- }
34
- end
35
-
36
- def handle_client_specific_errors(response, error)
37
- # Groq likely uses 'code' like OpenAI since it's OpenAI-compatible
38
- error_code = error["code"]
39
-
40
- case response.code.to_i
41
-
42
- when 413
43
- if error["message"]&.start_with?("Request too large")
44
- raise Errors::PromptTooLong.new(error["message"], error["type"])
45
- end
46
- when 429
47
- raise Errors::RateLimitError.new(error["type"], error_code) if error_code == "rate_limit_exceeded"
48
-
49
- raise Errors::OverloadError.new(error["message"], error_code)
50
- end
51
-
52
- # If we get here, we didn't handle it specifically
53
- raise Errors::APIStatusError.new(error["message"], error_code)
54
- end
55
- end
56
- end
57
- end
58
- end
@@ -1,105 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmGateway
4
- module Adapters
5
- module Groq
6
- class InputMapper
7
- def self.map(data)
8
- {
9
- messages: map_messages(data[:messages]),
10
- response_format: map_response_format(data[:response_format]),
11
- tools: map_tools(data[:tools]),
12
- system: map_system(data[:system])
13
- }
14
- end
15
-
16
- private
17
-
18
- def self.map_system(system)
19
- system
20
- end
21
-
22
- def self.map_response_format(response_format)
23
- response_format
24
- end
25
-
26
- def self.map_messages(messages)
27
- return messages unless messages
28
-
29
- messages.flat_map do |msg|
30
- if msg[:content].is_a?(Array)
31
- # Handle array content with tool calls and tool results
32
- tool_calls = []
33
- regular_content = []
34
- tool_messages = []
35
-
36
- msg[:content].each do |content|
37
- case content[:type]
38
- when "tool_result"
39
- tool_messages << map_tool_result_message(content)
40
- when "tool_use"
41
- tool_calls << map_tool_usage(content)
42
- else
43
- regular_content << content
44
- end
45
- end
46
-
47
- result = []
48
-
49
- # Add the main message with tool calls if any
50
- if tool_calls.any? || regular_content.any?
51
- main_msg = msg.dup
52
- main_msg[:role] = "assistant" if !main_msg[:role]
53
- main_msg[:tool_calls] = tool_calls if tool_calls.any?
54
- main_msg[:content] = regular_content.any? ? regular_content : nil
55
- result << main_msg
56
- end
57
-
58
- # Add separate tool result messages
59
- result += tool_messages
60
-
61
- result
62
- else
63
- # Regular message, return as-is
64
- [ msg ]
65
- end
66
- end
67
- end
68
-
69
- def self.map_tools(tools)
70
- return tools unless tools
71
-
72
- tools.map do |tool|
73
- {
74
- type: "function",
75
- function: {
76
- name: tool[:name],
77
- description: tool[:description],
78
- parameters: tool[:input_schema]
79
- }
80
- }
81
- end
82
- end
83
-
84
- def self.map_tool_usage(content)
85
- {
86
- 'id': content[:id],
87
- 'type': "function",
88
- 'function': {
89
- 'name': content[:name],
90
- 'arguments': content[:input].to_json
91
- }
92
- }
93
- end
94
-
95
- def self.map_tool_result_message(content)
96
- {
97
- role: "tool",
98
- tool_call_id: content[:tool_use_id],
99
- content: content[:content]
100
- }
101
- end
102
- end
103
- end
104
- end
105
- end
@@ -1,62 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmGateway
4
- module Adapters
5
- module Groq
6
- class OutputMapper
7
- def self.map(data)
8
- {
9
- id: data[:id],
10
- model: data[:model],
11
- usage: data[:usage],
12
- choices: map_choices(data[:choices])
13
- }
14
- end
15
-
16
- private
17
-
18
- def self.map_choices(choices)
19
- return [] unless choices
20
-
21
- choices.map do |choice|
22
- message = choice[:message] || {}
23
- content_item = map_content_item(message)
24
- tool_calls = map_tool_calls(message[:tool_calls])
25
-
26
- # Only include content_item if it has actual text content
27
- content_array = []
28
- content_array << content_item if LlmGateway::Utils.present?(content_item[:text])
29
- content_array += tool_calls
30
-
31
- { content: content_array }
32
- end
33
- end
34
-
35
- def self.map_content_item(message)
36
- {
37
- text: message[:content],
38
- type: "text"
39
- }
40
- end
41
-
42
- def self.map_tool_calls(tool_calls)
43
- return [] unless tool_calls
44
-
45
- tool_calls.map do |tool_call|
46
- {
47
- id: tool_call[:id],
48
- type: "tool_use",
49
- name: tool_call.dig(:function, :name),
50
- input: parse_tool_arguments(tool_call.dig(:function, :arguments))
51
- }
52
- end
53
- end
54
-
55
- def self.parse_tool_arguments(arguments)
56
- return arguments unless arguments.is_a?(String)
57
- JSON.parse(arguments, symbolize_names: true)
58
- end
59
- end
60
- end
61
- end
62
- end
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../../base_client"
4
-
5
- module LlmGateway
6
- module Adapters
7
- module OpenAi
8
- class Client < BaseClient
9
- def initialize(model_key: "gpt-4o", api_key: ENV["OPENAI_API_KEY"])
10
- @base_endpoint = "https://api.openai.com/v1"
11
- super(model_key: model_key, api_key: api_key)
12
- end
13
-
14
- def chat(messages, response_format: { type: "text" }, tools: nil, system: [], max_completion_tokens: 4096)
15
- body = {
16
- model: model_key,
17
- messages: system + messages,
18
- max_completion_tokens: max_completion_tokens
19
- }
20
- body[:tools] = tools if tools
21
-
22
- post("chat/completions", body)
23
- end
24
-
25
- def generate_embeddings(input)
26
- body = {
27
- input:,
28
- model: model_key
29
- }
30
- post("embeddings", body)
31
- end
32
-
33
- private
34
-
35
- def build_headers
36
- {
37
- "content-type" => "application/json",
38
- "Authorization" => "Bearer #{api_key}"
39
- }
40
- end
41
-
42
- def handle_client_specific_errors(response, error)
43
- # OpenAI uses 'code' instead of 'type' for error codes
44
- error_code = error["code"]
45
-
46
- case response.code.to_i
47
- when 429
48
- raise Errors::RateLimitError.new(error["message"], error_code)
49
- when 503
50
- raise Errors::OverloadError.new(error["message"], error_code)
51
- end
52
-
53
- # If we get here, we didn't handle it specifically
54
- raise Errors::APIStatusError.new(error["message"], error_code)
55
- end
56
- end
57
- end
58
- end
59
- end
@@ -1,63 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "base64"
4
-
5
- module LlmGateway
6
- module Adapters
7
- module OpenAi
8
- class InputMapper < LlmGateway::Adapters::Groq::InputMapper
9
- def self.map(data)
10
- {
11
- messages: map_messages(data[:messages]),
12
- response_format: map_response_format(data[:response_format]),
13
- tools: map_tools(data[:tools]),
14
- system: map_system(data[:system])
15
- }
16
- end
17
-
18
- private
19
-
20
- def self.map_messages(messages)
21
- return messages unless messages
22
-
23
- # First, handle file transformations
24
- messages_with_files = messages.map do |msg|
25
- if msg[:content].is_a?(Array)
26
- content = msg[:content].map do |content|
27
- if content[:type] == "file"
28
- # Map text/plain to application/pdf for OpenAI
29
- media_type = content[:media_type] == "text/plain" ? "application/pdf" : content[:media_type]
30
- {
31
- type: "file",
32
- file: {
33
- filename: content[:name],
34
- file_data: "data:#{media_type};base64,#{Base64.encode64(content[:data])}"
35
- }
36
- }
37
- else
38
- content
39
- end
40
- end
41
- msg.merge(content: content)
42
- else
43
- msg
44
- end
45
- end
46
-
47
- # Then apply parent's tool transformation logic
48
- super(messages_with_files)
49
- end
50
-
51
- def self.map_system(system)
52
- if !system || system.empty?
53
- []
54
- else
55
- system.map do |msg|
56
- msg[:role] == "system" ? msg.merge(role: "developer") : msg
57
- end
58
- end
59
- end
60
- end
61
- end
62
- end
63
- end
@@ -1,65 +0,0 @@
1
- class Agent
2
- def initialize(prompt_class, model, api_key)
3
- @prompt_class = prompt_class
4
- @model = model
5
- @api_key = api_key
6
- @transcript = []
7
- end
8
-
9
- def run(user_input, &block)
10
- @transcript << { role: 'user', content: [ { type: 'text', text: user_input } ] }
11
-
12
- begin
13
- prompt = @prompt_class.new(@model, @transcript, @api_key)
14
- result = prompt.post
15
- process_response(result[:choices][0][:content], &block)
16
- rescue => e
17
- yield({ type: 'error', message: e.message }) if block_given?
18
- raise e
19
- end
20
- end
21
-
22
- private
23
-
24
- def process_response(response, &block)
25
- @transcript << { role: 'assistant', content: response }
26
-
27
- response.each do |message|
28
- yield(message) if block_given?
29
-
30
- if message[:type] == 'text'
31
- # Text response processed
32
- elsif message[:type] == 'tool_use'
33
- result = handle_tool_use(message)
34
-
35
- tool_result = {
36
- type: 'tool_result',
37
- tool_use_id: message[:id],
38
- content: result
39
- }
40
- @transcript << { role: 'user', content: [ tool_result ] }
41
-
42
- yield(tool_result) if block_given?
43
-
44
- follow_up_prompt = @prompt_class.new(@model, @transcript, @api_key)
45
- follow_up = follow_up_prompt.post
46
-
47
- process_response(follow_up[:choices][0][:content], &block) if follow_up[:choices][0][:content]
48
- end
49
- end
50
-
51
- response
52
- end
53
-
54
- def handle_tool_use(message)
55
- tool_class = @prompt_class.find_tool(message[:name])
56
- if tool_class
57
- tool = tool_class.new
58
- tool.execute(message[:input])
59
- else
60
- "Unknown tool: #{message[:name]}"
61
- end
62
- rescue StandardError => e
63
- "Error executing tool: #{e.message}"
64
- end
65
- end
@@ -1,40 +0,0 @@
1
- require_relative 'prompt'
2
- require_relative 'agent'
3
- require 'debug'
4
-
5
- # Bash File Search Assistant using LlmGateway architecture
6
-
7
- class ClaudeCloneClone
8
- def initialize(model, api_key)
9
- @agent = Agent.new(Prompt, model, api_key)
10
- end
11
-
12
- def query(input)
13
- begin
14
- @agent.run(input) do |message|
15
- case message[:type]
16
- when 'text'
17
- puts "\n\e[32m•\e[0m #{message[:text]}"
18
- when 'tool_use'
19
- puts "\n\e[33m•\e[0m \e[36m#{message[:name]}\e[0m"
20
- if message[:input] && !message[:input].empty?
21
- puts " \e[90m#{message[:input]}\e[0m"
22
- end
23
- when 'tool_result'
24
- if message[:content] && !message[:content].empty?
25
- content_preview = message[:content].to_s.split("\n").first(3).join("\n")
26
- if content_preview.length > 100
27
- content_preview = content_preview[0..97] + "..."
28
- end
29
- puts " \e[90m#{content_preview}\e[0m"
30
- end
31
- when 'error'
32
- puts "\n\e[31m•\e[0m \e[91mError: #{message[:message]}\e[0m"
33
- end
34
- end
35
- rescue => e
36
- puts "\n\e[31m•\e[0m \e[91mError: #{e.message}\e[0m"
37
- puts "\e[90m #{e.backtrace.first}\e[0m" if e.backtrace&.first
38
- end
39
- end
40
- end
@@ -1,79 +0,0 @@
1
- require_relative 'tools/edit_tool'
2
- require_relative 'tools/read_tool'
3
- require_relative 'tools/todowrite_tool'
4
- require_relative 'tools/bash_tool'
5
- require_relative 'tools/grep_tool'
6
-
7
- class Prompt < LlmGateway::Prompt
8
- def initialize(model, transcript, api_key)
9
- super(model)
10
- @transcript = transcript
11
- @api_key = api_key
12
- end
13
-
14
- def prompt
15
- @transcript
16
- end
17
-
18
- def system_prompt
19
- <<~SYSTEM
20
- You are Claude Code Clone, an interactive CLI tool that assists with software engineering tasks.
21
-
22
- # Core Capabilities
23
-
24
- I provide assistance with:
25
- - Code analysis and debugging
26
- - Feature implementation
27
- - File editing and creation
28
- - Running tests and builds
29
- - Git operations
30
- - Web browsing and research
31
- - Task planning and management
32
-
33
- ## Available Tools
34
-
35
- You have access to these specialized tools:
36
- - `Edit` - Modify existing files by replacing specific text strings
37
- - `Read` - Read file contents with optional pagination
38
- - `TodoWrite` - Create and manage structured task lists
39
- - `Bash` - Execute shell commands with timeout support
40
- - `Grep` - Search for patterns in files using regex
41
-
42
- ## Core Instructions
43
-
44
- I am designed to:
45
- - Be concise and direct (minimize output tokens)
46
- - Follow existing code conventions and patterns
47
- - Use defensive security practices only
48
- - Plan tasks with the TodoWrite tool for complex work
49
- - Run linting/typechecking after making changes
50
- - Never commit unless explicitly asked
51
-
52
- ## Process
53
-
54
- 1. **Understand the Request**: Parse what the user needs accomplished
55
- 2. **Plan if Complex**: Use TodoWrite for multi-step tasks
56
- 3. **Execute Tools**: Use appropriate tools to complete the work
57
- 4. **Validate**: Run tests/linting when applicable
58
- 5. **Report**: Provide concise status updates
59
-
60
- Always use the available tools to perform actions rather than just suggesting commands.
61
-
62
- Before starting any task, build a todo list of what you need to do, ensuring each item is actionable and prioritized. Then, execute the tasks one by one, using the TodoWrite tool to track progress and completion.
63
-
64
- After completing each task, update the TodoWrite list to reflect the status and any necessary follow-up actions.
65
- SYSTEM
66
- end
67
-
68
- def self.tools
69
- [ EditTool, ReadTool, TodoWriteTool, BashTool, GrepTool ]
70
- end
71
-
72
- def tools
73
- self.class.tools.map(&:definition)
74
- end
75
-
76
- def post
77
- LlmGateway::Client.chat(model, prompt, tools: tools, system: system_prompt, api_key: @api_key)
78
- end
79
- end
@@ -1,47 +0,0 @@
1
- require 'tty-prompt'
2
- require_relative '../../lib/llm_gateway'
3
- require_relative 'claude_code_clone.rb'
4
-
5
- # Terminal Runner for FileSearchBot
6
- class FileSearchTerminalRunner
7
- def initialize
8
- @prompt = TTY::Prompt.new
9
- end
10
-
11
- def start
12
- puts "First, let's configure your LLM settings:\n\n"
13
-
14
- model, api_key = setup_configuration
15
- bot = ClaudeCloneClone.new(model, api_key)
16
-
17
- puts "Type 'quit' or 'exit' to stop.\n\n"
18
-
19
- loop do
20
- user_input = @prompt.ask("What can i do for you?")
21
-
22
- break if [ 'quit', 'exit' ].include?(user_input.downcase)
23
-
24
- bot.query(user_input)
25
- end
26
- end
27
-
28
- private
29
-
30
- def setup_configuration
31
- model = @prompt.ask("Enter model (default: claude-3-7-sonnet-20250219):") do |q|
32
- q.default 'claude-3-7-sonnet-20250219'
33
- end
34
-
35
- api_key = @prompt.mask("Enter your API key:") do |q|
36
- q.required true
37
- end
38
-
39
- [ model, api_key ]
40
- end
41
- end
42
-
43
- # Start the bot
44
- if __FILE__ == $0
45
- runner = FileSearchTerminalRunner.new
46
- runner.start
47
- end