soka 0.0.4 → 0.0.6

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.
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Soka
4
+ module Engines
5
+ module Prompts
6
+ # Helper methods for formatting prompt components
7
+ module FormatHelpers
8
+ private
9
+
10
+ # Format tools description for prompt
11
+ # @param tools [Array] The tools available
12
+ # @return [String] Formatted tools description
13
+ def format_tools_description(tools)
14
+ return 'No tools available.' if tools.empty?
15
+
16
+ tools.map do |tool|
17
+ schema = tool.class.to_h
18
+ params_desc = format_parameters(schema[:parameters])
19
+
20
+ "- #{schema[:name]}: #{schema[:description]}\n Parameters: \n#{params_desc}"
21
+ end.join("\n")
22
+ end
23
+
24
+ # Format parameters for tool description
25
+ # @param params_schema [Hash] The parameters schema
26
+ # @return [String] Formatted parameters description
27
+ def format_parameters(params_schema)
28
+ return 'none' if params_schema[:properties].empty?
29
+
30
+ properties = params_schema[:properties].map do |name, config|
31
+ required = params_schema[:required].include?(name.to_s) ? '(required)' : '(optional)'
32
+ type = config[:type]
33
+ desc = config[:description]
34
+
35
+ " - #{name} #{required} [#{type}] - #{desc}"
36
+ end
37
+
38
+ properties.join("\n")
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Soka
4
+ module Engines
5
+ module Prompts
6
+ # Module for building instruction components
7
+ module Instructions
8
+ private
9
+
10
+ # Build iteration limit warning
11
+ # @return [String] The iteration limit warning
12
+ def build_iteration_limit_warning
13
+ return '' unless respond_to?(:max_iterations) && max_iterations
14
+
15
+ <<~WARNING
16
+ ⏰ ITERATION LIMIT: You have a maximum of #{max_iterations} iterations to complete this task.
17
+ - Each Thought/Action cycle counts as one iteration
18
+ - Plan your approach efficiently to stay within this limit
19
+ - If you cannot complete the task within #{max_iterations} iterations, provide your best answer with what you have
20
+ WARNING
21
+ end
22
+
23
+ # Build thinking instruction based on language
24
+ # @param language [String, nil] The language to use for thinking
25
+ # @return [String] The thinking instruction
26
+ def build_thinking_instruction(language)
27
+ return '' unless language
28
+
29
+ <<~INSTRUCTION
30
+ 🌐 THINKING LANGUAGE:
31
+ Use #{language} for your reasoning WITHIN the <Thought> XML tags.
32
+ This affects the content inside tags, not the tag structure itself.
33
+ INSTRUCTION
34
+ end
35
+
36
+ # Step format example
37
+ # @return [String] The step format example
38
+ def step_format_example
39
+ <<~FORMAT
40
+ 📝 XML TAG FORMAT FOR EACH STEP:
41
+
42
+ 1️⃣ THINKING PHASE (Required):
43
+ <Thought>
44
+ MAXIMUM 30 WORDS - Be extremely concise!
45
+ Use first-person perspective (I, me, my).
46
+ NEVER mention: "LLM", "AI", "formatting for", "organizing for someone".
47
+ NEVER say: "I will act as", "I will play the role of", "as an expert".
48
+ BE DIRECT: "I'll check", "Let me see", "Looking at this".
49
+ Keep it light and witty within the word limit.
50
+ </Thought>
51
+
52
+ 2️⃣ ACTION PHASE (When needed):
53
+ <Action>
54
+ {"tool": "tool_name", "parameters": {"param1": "value1", "param2": "value2"}}
55
+ </Action>
56
+
57
+ ⚠️ CRITICAL: Each tag MUST have both opening <TagName> and closing </TagName>.
58
+ The content between tags follows any custom instructions provided.
59
+ FORMAT
60
+ end
61
+
62
+ # Critical format for final answer
63
+ # @return [String] The critical format instructions
64
+ def final_answer_critical_format
65
+ <<~CRITICAL
66
+ ⚠️ CRITICAL - FINAL ANSWER XML TAG FORMAT:
67
+ When you have the complete answer, YOU MUST use XML-style tags:
68
+
69
+ <FinalAnswer>
70
+ State the result directly without explanation.
71
+ No justification or reasoning needed - just the answer.
72
+ Maximum 300 words. Be direct and concise.
73
+ </FinalAnswer>
74
+
75
+ 🚨 PARSER REQUIREMENTS:
76
+ - The system parser REQUIRES both opening <FinalAnswer> and closing </FinalAnswer> tags
77
+ - Without proper XML tags, the system CANNOT detect task completion
78
+ - The tags are case-sensitive and must match exactly
79
+ - Final answer MUST NOT exceed 300 words
80
+ - NO over-explanation - direct results only
81
+ CRITICAL
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Soka
4
+ module Engines
5
+ module Prompts
6
+ # Module for ReAct workflow rules
7
+ module WorkflowRules
8
+ private
9
+
10
+ # Action format rules
11
+ # @return [String] The action format rules
12
+ def action_format_rules
13
+ <<~RULES
14
+ 🔄 REACT WORKFLOW WITH XML TAGS:
15
+
16
+ STOP HERE after each Action. Do NOT include <Observation> in your response.
17
+ The system will execute the tool and provide the observation wrapped in <Observation> tags.
18
+
19
+ After receiving the observation, you can continue with more Thought/Action cycles or provide a final answer.
20
+
21
+ #{react_flow_example}
22
+
23
+ #{xml_tag_requirements}
24
+ RULES
25
+ end
26
+
27
+ # ReAct flow example
28
+ # @return [String] The ReAct flow example
29
+ def react_flow_example
30
+ <<~EXAMPLE
31
+ 📝 EXAMPLE OF COMPLETE REACT FLOW WITH XML TAGS:
32
+
33
+ Step 1: Your response
34
+ <Thought>Math problem! I'll use the calculator tool for 2+2.</Thought>
35
+ <Action>{"tool": "calculator", "parameters": {"expression": "2+2"}}</Action>
36
+
37
+ Step 2: System provides
38
+ <Observation>4</Observation>
39
+
40
+ Step 3: Your response
41
+ <Thought>Got it - the answer is 4!</Thought>
42
+ <FinalAnswer>4</FinalAnswer>
43
+ EXAMPLE
44
+ end
45
+
46
+ # XML tag requirements
47
+ # @return [String] The XML tag requirements
48
+ def xml_tag_requirements
49
+ <<~REQUIREMENTS
50
+ 🚨 XML TAG REQUIREMENTS AND RULES:
51
+
52
+ 1. 🏷️ MANDATORY XML STRUCTURE:
53
+ - Always wrap content in appropriate XML tags
54
+ - Tags: <Thought>, <Action>, <FinalAnswer>
55
+ - Each tag MUST have matching closing tag
56
+
57
+ 2. 💭 THINKING PHASE:
58
+ - Always start with <Thought>...</Thought>
59
+ - MAXIMUM 30 WORDS per thought
60
+ - Use first-person perspective (I, me, my)
61
+ - AVOID: "LLM", "AI", "formatting for", "organizing for"
62
+ - AVOID: "act as", "play role of", "as an expert", "I will be"
63
+ - BE DIRECT: Use natural language like "I'll check", "Let me see"
64
+ - Be concise and witty
65
+ - Custom instructions affect HOW you think, not WHETHER you use tags
66
+
67
+ 3. 🔧 ACTION FORMAT:
68
+ - <Action> content MUST be valid JSON on a single line
69
+ - Format: {"tool": "name", "parameters": {...}}
70
+ - Empty parameters: {"tool": "name", "parameters": {}}
71
+
72
+ 4. 👁️ OBSERVATION:
73
+ - NEVER create <Observation> tags yourself
74
+ - System provides these automatically
75
+
76
+ 5. ✅ FINAL ANSWER:
77
+ - MUST use <FinalAnswer>...</FinalAnswer> tags
78
+ - Both opening AND closing tags REQUIRED
79
+ - System cannot detect completion without these tags
80
+ - No text after closing </FinalAnswer> tag
81
+ - Maximum 300 words
82
+ - Direct results only - NO explanations or justifications
83
+
84
+ 6. 🎯 EFFICIENCY:
85
+ - Limited iterations available
86
+ - Think harder upfront to minimize tool calls
87
+ - Prioritize essential actions
88
+
89
+ 7. 📋 CUSTOM INSTRUCTIONS SCOPE:
90
+ - Custom instructions modify content WITHIN tags
91
+ - They DO NOT change the XML tag structure requirement
92
+ - ReAct workflow with XML tags is ALWAYS required
93
+ REQUIREMENTS
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Soka
4
+ module Engines
5
+ # Main module that combines all prompt components
6
+ module Prompts
7
+ include Base
8
+ include Instructions
9
+ include WorkflowRules
10
+ include FormatHelpers
11
+ end
12
+ end
13
+ end
@@ -1,17 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
-
5
3
  module Soka
6
4
  module Engines
7
5
  # ReAct (Reasoning and Acting) engine implementation
8
6
  class React < Base
7
+ include Prompts
9
8
  include Concerns::ResponseProcessor
10
- include Concerns::PromptTemplate
11
9
  include Concerns::ResponseParser
12
10
  include Concerns::ResultBuilder
13
11
 
14
- ReasonResult = Struct.new(:input, :thoughts, :final_answer, :status, :error, :confidence_score,
12
+ ReasonResult = Struct.new(:input, :thoughts, :final_answer, :status, :error,
15
13
  keyword_init: true) do
16
14
  def successful?
17
15
  status == :success
@@ -35,12 +33,13 @@ module Soka
35
33
  # @param context [ReasoningContext] The reasoning context
36
34
  # @return [ReasonResult, nil] The result if found, nil otherwise
37
35
  def iterate_reasoning(context)
38
- max_iterations.times do
36
+ max_iterations.times do |index|
37
+ context.iteration = index # Set current iteration number
39
38
  result = process_iteration(context)
40
39
  return result if result
41
-
42
- context.increment_iteration!
43
40
  end
41
+ # When max iterations reached, ensure iteration count is correct
42
+ context.iteration = max_iterations
44
43
  nil
45
44
  end
46
45
 
@@ -37,18 +37,6 @@ module Soka
37
37
  @event_handler.call(event)
38
38
  end
39
39
 
40
- # Check if we've reached the maximum number of iterations
41
- # @return [Boolean] true if max iterations reached
42
- def max_iterations_reached?
43
- @iteration >= @max_iterations
44
- end
45
-
46
- # Increment the iteration counter
47
- # @return [Integer] The new iteration count
48
- def increment_iteration!
49
- @iteration += 1
50
- end
51
-
52
40
  # Get the current iteration number (1-based for display)
53
41
  # @return [Integer] The current iteration number for display
54
42
  def current_step
data/lib/soka/llm.rb CHANGED
@@ -69,14 +69,14 @@ module Soka
69
69
  # @param provider_name [Symbol] Provider type
70
70
  # @param options [Hash] Provider options
71
71
  # @return [LLMs::Base] Provider instance
72
- def create_provider(provider_name, **)
72
+ def create_provider(provider_name, ...)
73
73
  case provider_name.to_sym
74
74
  when :gemini
75
- LLMs::Gemini.new(**)
75
+ LLMs::Gemini.new(...)
76
76
  when :openai
77
- LLMs::OpenAI.new(**)
77
+ LLMs::OpenAI.new(...)
78
78
  when :anthropic
79
- LLMs::Anthropic.new(**)
79
+ LLMs::Anthropic.new(...)
80
80
  else
81
81
  raise LLMError, "Unknown LLM provider: #{provider_name}"
82
82
  end
@@ -6,6 +6,21 @@ module Soka
6
6
  class Anthropic < Base
7
7
  ENV_KEY = 'ANTHROPIC_API_KEY'
8
8
 
9
+ def chat(messages, **params)
10
+ request_params = build_request_params(messages, params)
11
+
12
+ response = connection.post do |req|
13
+ req.url '/v1/messages'
14
+ req.headers['x-api-key'] = api_key
15
+ req.headers['anthropic-version'] = options[:anthropic_version]
16
+ req.body = request_params
17
+ end
18
+
19
+ parse_response(response)
20
+ rescue Faraday::Error => e
21
+ handle_error(e)
22
+ end
23
+
9
24
  private
10
25
 
11
26
  def default_model
@@ -21,30 +36,11 @@ module Soka
21
36
  temperature: 0.7,
22
37
  top_p: 1.0,
23
38
  top_k: 1,
24
- max_tokens: 2048,
25
- anthropic_version: '2023-06-01'
39
+ anthropic_version: '2023-06-01',
40
+ max_tokens: 2048
26
41
  }
27
42
  end
28
43
 
29
- public
30
-
31
- def chat(messages, **params)
32
- request_params = build_request_params(messages, params)
33
-
34
- response = connection.post do |req|
35
- req.url '/v1/messages'
36
- req.headers['x-api-key'] = api_key
37
- req.headers['anthropic-version'] = options[:anthropic_version]
38
- req.body = request_params
39
- end
40
-
41
- parse_response(response)
42
- rescue Faraday::Error => e
43
- handle_error(e)
44
- end
45
-
46
- private
47
-
48
44
  def build_request_params(messages, params)
49
45
  formatted_messages, system_prompt = extract_system_prompt(messages)
50
46
  request = build_base_request(formatted_messages, params)
@@ -57,8 +57,7 @@ module Soka
57
57
  faraday.request :json
58
58
  faraday.response :json
59
59
  faraday.adapter Faraday.default_adapter
60
- faraday.options.timeout = options[:timeout] || 30
61
- faraday.options.open_timeout = options[:open_timeout] || 10
60
+ faraday.options.timeout = 60
62
61
  end
63
62
  end
64
63
 
@@ -20,8 +20,7 @@ module Soka
20
20
  {
21
21
  temperature: 0.7,
22
22
  top_p: 1.0,
23
- top_k: 1,
24
- max_output_tokens: 2048
23
+ top_k: 1
25
24
  }
26
25
  end
27
26
 
@@ -50,7 +49,7 @@ module Soka
50
49
  temperature: params[:temperature] || options[:temperature],
51
50
  topP: params[:top_p] || options[:top_p],
52
51
  topK: params[:top_k] || options[:top_k],
53
- maxOutputTokens: params[:max_output_tokens] || options[:max_output_tokens]
52
+ thinkingConfig: { thinkingBudget: 512 }
54
53
  }
55
54
  }
56
55
  end
@@ -6,33 +6,11 @@ module Soka
6
6
  class OpenAI < Base
7
7
  ENV_KEY = 'OPENAI_API_KEY'
8
8
 
9
- private
10
-
11
- def default_model
12
- 'gpt-4.1-mini'
13
- end
14
-
15
- def base_url
16
- 'https://api.openai.com'
17
- end
18
-
19
- def default_options
20
- {
21
- temperature: 0.7,
22
- top_p: 1.0,
23
- frequency_penalty: 0,
24
- presence_penalty: 0,
25
- max_tokens: 2048
26
- }
27
- end
28
-
29
- public
30
-
31
9
  def chat(messages, **params)
32
10
  request_params = build_request_params(messages, params)
33
11
 
34
12
  response = connection.post do |req|
35
- req.url '/v1/chat/completions'
13
+ req.url '/v1/responses'
36
14
  req.headers['Authorization'] = "Bearer #{api_key}"
37
15
  req.body = request_params
38
16
  end
@@ -44,16 +22,35 @@ module Soka
44
22
 
45
23
  private
46
24
 
25
+ def default_model
26
+ 'gpt-5-mini'
27
+ end
28
+
29
+ def base_url
30
+ 'https://api.openai.com'
31
+ end
32
+
47
33
  def build_request_params(messages, params)
48
- {
34
+ request_params = {
49
35
  model: model,
50
- messages: messages,
51
- temperature: params[:temperature] || options[:temperature],
52
- top_p: params[:top_p] || options[:top_p],
53
- frequency_penalty: params[:frequency_penalty] || options[:frequency_penalty],
54
- presence_penalty: params[:presence_penalty] || options[:presence_penalty],
55
- max_tokens: params[:max_tokens] || options[:max_tokens]
36
+ input: messages
56
37
  }
38
+
39
+ # Add max_output_tokens if provided (Responses API uses max_output_tokens)
40
+ request_params[:max_output_tokens] = params[:max_tokens] if params[:max_tokens]
41
+ add_reasoning_effort(request_params)
42
+
43
+ request_params
44
+ end
45
+
46
+ def add_reasoning_effort(request_params)
47
+ return unless allowed_reasoning_prefix_models?
48
+
49
+ request_params[:reasoning] = { effort: 'minimal', summary: 'auto' }
50
+ end
51
+
52
+ def allowed_reasoning_prefix_models?
53
+ model.start_with?('gpt-5') && !model.start_with?('gpt-5-chat-latest')
57
54
  end
58
55
 
59
56
  def parse_response(response)
@@ -70,18 +67,27 @@ module Soka
70
67
  end
71
68
 
72
69
  def build_result_from_response(body)
73
- choice = body.dig('choices', 0)
74
- message = choice['message']
70
+ # Extract text from the Responses API format
71
+ output_text = extract_output_text(body['output'])
72
+
73
+ # Get the status to determine finish reason
74
+ finish_reason = body['status'] == 'completed' ? 'stop' : body['status']
75
75
 
76
76
  Result.new(
77
77
  model: body['model'],
78
- content: message['content'],
79
- input_tokens: body.dig('usage', 'prompt_tokens'),
80
- output_tokens: body.dig('usage', 'completion_tokens'),
81
- finish_reason: choice['finish_reason'],
78
+ content: output_text,
79
+ input_tokens: body.dig('usage', 'input_tokens'),
80
+ output_tokens: body.dig('usage', 'output_tokens'),
81
+ finish_reason: finish_reason,
82
82
  raw_response: body
83
83
  )
84
84
  end
85
+
86
+ def extract_output_text(output_items)
87
+ message = output_items.find { |item| item['type'] == 'message' }
88
+ content = message['content'].find { |content| content['type'] == 'output_text' }
89
+ content['text']
90
+ end
85
91
  end
86
92
  end
87
93
  end
data/lib/soka/result.rb CHANGED
@@ -37,12 +37,6 @@ module Soka
37
37
  status == :failed
38
38
  end
39
39
 
40
- # Check if the result timed out
41
- # @return [Boolean]
42
- def timeout?
43
- status == :timeout
44
- end
45
-
46
40
  # Check if max iterations were reached
47
41
  # @return [Boolean]
48
42
  def max_iterations_reached?
@@ -66,7 +60,7 @@ module Soka
66
60
  # Convert to JSON string
67
61
  # @return [String]
68
62
  def to_json(*)
69
- to_h.to_json(*)
63
+ Oj.dump(to_h, mode: :compat)
70
64
  end
71
65
 
72
66
  # Get a summary of the result
@@ -117,7 +111,6 @@ module Soka
117
111
  {
118
112
  success: "Success: #{truncate(final_answer)}",
119
113
  failed: "Failed: #{error}",
120
- timeout: 'Timeout: Execution exceeded time limit',
121
114
  max_iterations_reached: "Max iterations reached: #{iterations} iterations"
122
115
  }
123
116
  end
data/lib/soka/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Soka
4
- VERSION = '0.0.4'
4
+ VERSION = '0.0.6'
5
5
  end
data/lib/soka.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'zeitwerk'
4
4
  require 'faraday'
5
+ require 'oj'
5
6
 
6
7
  # Main module for the Soka ReAct Agent Framework
7
8
  # Provides AI agent capabilities with multiple LLM providers support
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: soka
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - jiunjiun
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2025-08-12 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: faraday
@@ -23,6 +24,20 @@ dependencies:
23
24
  - - "~>"
24
25
  - !ruby/object:Gem::Version
25
26
  version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: oj
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.16'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.16'
26
41
  - !ruby/object:Gem::Dependency
27
42
  name: zeitwerk
28
43
  requirement: !ruby/object:Gem::Requirement
@@ -75,10 +90,14 @@ files:
75
90
  - lib/soka/agents/tool_builder.rb
76
91
  - lib/soka/configuration.rb
77
92
  - lib/soka/engines/base.rb
78
- - lib/soka/engines/concerns/prompt_template.rb
79
93
  - lib/soka/engines/concerns/response_parser.rb
80
94
  - lib/soka/engines/concerns/response_processor.rb
81
95
  - lib/soka/engines/concerns/result_builder.rb
96
+ - lib/soka/engines/prompts.rb
97
+ - lib/soka/engines/prompts/base.rb
98
+ - lib/soka/engines/prompts/format_helpers.rb
99
+ - lib/soka/engines/prompts/instructions.rb
100
+ - lib/soka/engines/prompts/workflow_rules.rb
82
101
  - lib/soka/engines/react.rb
83
102
  - lib/soka/engines/reasoning_context.rb
84
103
  - lib/soka/llm.rb
@@ -98,6 +117,7 @@ metadata:
98
117
  source_code_uri: https://github.com/jiunjiun/soka
99
118
  changelog_uri: https://github.com/jiunjiun/soka/blob/main/CHANGELOG.md
100
119
  rubygems_mfa_required: 'true'
120
+ post_install_message:
101
121
  rdoc_options: []
102
122
  require_paths:
103
123
  - lib
@@ -105,14 +125,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
105
125
  requirements:
106
126
  - - ">="
107
127
  - !ruby/object:Gem::Version
108
- version: '3.4'
128
+ version: '3.1'
109
129
  required_rubygems_version: !ruby/object:Gem::Requirement
110
130
  requirements:
111
131
  - - ">="
112
132
  - !ruby/object:Gem::Version
113
133
  version: '0'
114
134
  requirements: []
115
- rubygems_version: 3.6.9
135
+ rubygems_version: 3.3.27
136
+ signing_key:
116
137
  specification_version: 4
117
138
  summary: A Ruby ReAct Agent Framework with multi-LLM support
118
139
  test_files: []