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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -0
- data/README.md +565 -129
- data/Rakefile +8 -3
- data/docs/migration-guide.md +135 -0
- data/lib/llm_gateway/adapters/adapter.rb +173 -0
- data/lib/llm_gateway/adapters/anthropic/acts_like_messages.rb +23 -0
- data/lib/llm_gateway/adapters/anthropic/bidirectional_message_mapper.rb +111 -0
- data/lib/llm_gateway/adapters/{claude → anthropic}/input_mapper.rb +12 -10
- data/lib/llm_gateway/adapters/anthropic/messages_adapter.rb +19 -0
- data/lib/llm_gateway/adapters/anthropic/output_mapper.rb +50 -0
- data/lib/llm_gateway/adapters/anthropic/stream_mapper.rb +110 -0
- data/lib/llm_gateway/adapters/anthropic_option_mapper.rb +53 -0
- data/lib/llm_gateway/adapters/groq/chat_completions_adapter.rb +47 -0
- data/lib/llm_gateway/adapters/groq/option_mapper.rb +27 -0
- data/lib/llm_gateway/adapters/input_message_sanitizer.rb +93 -0
- data/lib/llm_gateway/adapters/openai/acts_like_chat_completions.rb +22 -0
- data/lib/llm_gateway/adapters/openai/acts_like_responses.rb +31 -0
- data/lib/llm_gateway/adapters/openai/chat_completions/bidirectional_message_mapper.rb +110 -0
- data/lib/llm_gateway/adapters/openai/chat_completions/input_mapper.rb +105 -0
- data/lib/llm_gateway/adapters/openai/chat_completions/input_message_sanitizer.rb +65 -0
- data/lib/llm_gateway/adapters/openai/chat_completions/option_mapper.rb +39 -0
- data/lib/llm_gateway/adapters/openai/chat_completions/output_mapper.rb +40 -0
- data/lib/llm_gateway/adapters/openai/chat_completions/stream_mapper.rb +242 -0
- data/lib/llm_gateway/adapters/openai/chat_completions_adapter.rb +20 -0
- data/lib/llm_gateway/adapters/openai/file_output_mapper.rb +25 -0
- data/lib/llm_gateway/adapters/openai/prompt_cache_option_mapper.rb +39 -0
- data/lib/llm_gateway/adapters/openai/responses/bidirectional_message_mapper.rb +120 -0
- data/lib/llm_gateway/adapters/openai/responses/input_mapper.rb +106 -0
- data/lib/llm_gateway/adapters/openai/responses/option_mapper.rb +41 -0
- data/lib/llm_gateway/adapters/openai/responses/output_mapper.rb +47 -0
- data/lib/llm_gateway/adapters/openai/responses/stream_mapper.rb +340 -0
- data/lib/llm_gateway/adapters/openai/responses_adapter.rb +20 -0
- data/lib/llm_gateway/adapters/openai_codex/input_mapper.rb +206 -0
- data/lib/llm_gateway/adapters/openai_codex/option_mapper.rb +28 -0
- data/lib/llm_gateway/adapters/openai_codex/responses_adapter.rb +38 -0
- data/lib/llm_gateway/adapters/{open_ai/output_mapper.rb → option_mapper.rb} +5 -2
- data/lib/llm_gateway/adapters/stream_accumulator.rb +91 -0
- data/lib/llm_gateway/adapters/structs.rb +145 -0
- data/lib/llm_gateway/base_client.rb +97 -1
- data/lib/llm_gateway/client.rb +66 -54
- data/lib/llm_gateway/clients/anthropic.rb +167 -0
- data/lib/llm_gateway/clients/claude_code/oauth_flow.rb +162 -0
- data/lib/llm_gateway/clients/claude_code/token_manager.rb +112 -0
- data/lib/llm_gateway/clients/groq.rb +54 -0
- data/lib/llm_gateway/clients/openai.rb +208 -0
- data/lib/llm_gateway/clients/openai_codex/oauth_flow.rb +258 -0
- data/lib/llm_gateway/clients/openai_codex/token_manager.rb +71 -0
- data/lib/llm_gateway/errors.rb +23 -0
- data/lib/llm_gateway/prompt.rb +12 -1
- data/lib/llm_gateway/provider_registry.rb +37 -0
- data/lib/llm_gateway/version.rb +1 -1
- data/lib/llm_gateway.rb +169 -10
- data/scripts/create_anthropic_credentials.rb +106 -0
- data/scripts/create_openai_codex_credentials.rb +116 -0
- data/scripts/generate_handoff_live_fixture.rb +169 -0
- data/scripts/generate_handoff_media_fixture.rb +167 -0
- metadata +64 -21
- data/lib/llm_gateway/adapters/claude/client.rb +0 -56
- data/lib/llm_gateway/adapters/claude/output_mapper.rb +0 -30
- data/lib/llm_gateway/adapters/groq/client.rb +0 -58
- data/lib/llm_gateway/adapters/groq/input_mapper.rb +0 -105
- data/lib/llm_gateway/adapters/groq/output_mapper.rb +0 -62
- data/lib/llm_gateway/adapters/open_ai/client.rb +0 -59
- data/lib/llm_gateway/adapters/open_ai/input_mapper.rb +0 -63
- data/sample/claude_code_clone/agent.rb +0 -65
- data/sample/claude_code_clone/claude_code_clone.rb +0 -40
- data/sample/claude_code_clone/prompt.rb +0 -79
- data/sample/claude_code_clone/run.rb +0 -47
- data/sample/claude_code_clone/tools/bash_tool.rb +0 -54
- data/sample/claude_code_clone/tools/edit_tool.rb +0 -61
- data/sample/claude_code_clone/tools/grep_tool.rb +0 -113
- data/sample/claude_code_clone/tools/read_tool.rb +0 -61
- 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
|