autoflux 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 10be8379032b489e881578406d74b7f9f0c80b480fc61b5ccf6c7e8e041f59ab
4
+ data.tar.gz: d1567e48c28a48cd432c6bb4aa5c8c1856e8b920c3c8768f5c0ac4764bdc8dcc
5
+ SHA512:
6
+ metadata.gz: acccc975d159aaf9d9262b2fa1cdc0a1cd2d8fc5a2bf3181121a721efe65d6731b733f7f588b0cfac30efe68d0979b14c54aca5b0bbcc0cb6a10bc8b0b38bfff
7
+ data.tar.gz: 5c99079282673226f90293ec9e0ec03ac65c1a658441bdf91511aadcc01ce2aa729d6accee8b7f864aa29b69e58b8eed9f4bc63ea6bc404e0048a799e0e4e1a5
data/.husky/commit-msg ADDED
@@ -0,0 +1 @@
1
+ npx --no -- commitlint --verbose --edit $1
data/.husky/pre-commit ADDED
@@ -0,0 +1 @@
1
+ bundle exec rake
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,14 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.0
3
+ NewCops: enable
4
+ SuggestExtensions: false
5
+
6
+ Style/StringLiterals:
7
+ EnforcedStyle: double_quotes
8
+
9
+ Style/StringLiteralsInInterpolation:
10
+ EnforcedStyle: double_quotes
11
+
12
+ Metrics/BlockLength:
13
+ Exclude:
14
+ - spec/**/*
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Aotokitsuruya
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,170 @@
1
+ Autoflux
2
+ ===
3
+
4
+ A lightweight AI agent framework
5
+
6
+ > [!WARNING]
7
+ > To support common agentic AI workflow, the API will be changed at any time until the design is completed.
8
+
9
+ ## Installation
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ ```bash
14
+ bundle add autoflux
15
+ ```
16
+
17
+ If bundler is not being used to manage dependencies, install the gem by executing:
18
+
19
+ ```bash
20
+ gem install autoflux
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ Autoflux provides a default state machine for a chat workflow.
26
+
27
+ ```mermaid
28
+ stateDiagram-v2
29
+ [*] --> Start
30
+ Start --> User
31
+ User --> Assistant
32
+ Assistant --> User
33
+ Assistant --> Tool
34
+ Tool --> Assistant
35
+ User --> Stop
36
+ Stop --> [*]
37
+ ```
38
+
39
+ To start a new workflow use `Autoflux::Workflow`:
40
+
41
+ ```ruby
42
+ workflow = Autoflux::Workflow.new(
43
+ agent: agent,
44
+ io: io,
45
+ )
46
+
47
+ workflow.run
48
+ ```
49
+
50
+ You can give a system prompt when running the workflow:
51
+
52
+ ```ruby
53
+ workflow.run(system_prompt: 'Help user to solve the problem')
54
+ ```
55
+
56
+ When receive "exit" from the user, the workflow transition to the stop state.
57
+
58
+ ### Agent
59
+
60
+ The agent is an adapter to the LLM model. You can ihnerit the `Autoflux::Agent` class to implement your own agent.
61
+
62
+ ```ruby
63
+ # :nodoc:
64
+ class OpenAIAgent < Autoflux::Agent
65
+ attr_reader :client, :model
66
+
67
+ def initialize(client:, tools: [], model: 'gpt-4o-mini')
68
+ super(tools: tools)
69
+ @client = client
70
+ @model = model
71
+ end
72
+
73
+ def call(memory:)
74
+ client.chat(
75
+ parameters: {
76
+ model: model,
77
+ messages: memory.data,
78
+ tools: tools
79
+ }
80
+ ).dig('choices', 0, 'message')
81
+ end
82
+
83
+ def tools
84
+ @tools ||= @_tools.map do |tool|
85
+ {
86
+ type: :function,
87
+ function: {
88
+ name: tool.name,
89
+ description: tool.description,
90
+ parameters: tool.parameters
91
+ }
92
+ }
93
+ end
94
+ end
95
+ end
96
+ ```
97
+
98
+ The memory is chat history which keep in the workflow. You can decide to use it or not.
99
+
100
+ ### Tool
101
+
102
+ The tool is a function that can be used in the agent's response. You can ihnerit the `Autoflux::Tool` class to implement your own tool.
103
+
104
+ ```ruby
105
+ # :nodoc:
106
+ class AddToCartTool < Autoflux::Tool
107
+ def initialize # rubocop:disable Metrics/MethodLength
108
+ super(
109
+ name: 'add_to_cart',
110
+ description: 'Add the product to the cart',
111
+ parameters: {
112
+ type: 'object',
113
+ properties: {
114
+ name: { type: 'string', description: 'The name of the product' },
115
+ quantity: { type: 'number', description: 'The quantity of the product' }
116
+ }
117
+ }
118
+ )
119
+ end
120
+
121
+ def call(name:, quantity:, **)
122
+ { success: true, content: "Added #{quantity} #{name} to the cart" }
123
+ end
124
+ end
125
+ ```
126
+
127
+ The tool is require the name and description. The parameters is optional.
128
+
129
+ ### IO
130
+
131
+ The IO is an adapter to the input and output. You can ihnerit the `Autoflux::IO` class to implement your own IO.
132
+
133
+ ```ruby
134
+ # :nodoc:
135
+ class ConsoleIO < Autoflux::IO
136
+ def input
137
+ print 'User: '
138
+ gets.chomp
139
+ end
140
+
141
+ def output(message)
142
+ puts "Assistant: #{message}"
143
+ end
144
+ end
145
+ ```
146
+
147
+ The default `Autoflux::Stdio` implement the minimal Standard I/O support.
148
+
149
+ ```ruby
150
+ workflow = Autoflux::Workflow.new(
151
+ agent: agent,
152
+ io: Autoflux::Stdio.new(prompt: '> ')
153
+ )
154
+
155
+ workflow.run
156
+ ```
157
+
158
+ ## Development
159
+
160
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
161
+
162
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
163
+
164
+ ## Contributing
165
+
166
+ Bug reports and pull requests are welcome on GitHub at https://github.com/elct9620/autoflux.
167
+
168
+ ## License
169
+
170
+ The gem is available as open source under the terms of the [Apache License 2.0](https://opensource.org/licenses/Apache-2.0).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1 @@
1
+ export default { extends: ['@commitlint/config-conventional'] };
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autoflux
4
+ # The Agent is a abstract class to represent the adapter of the Language Model.
5
+ class Agent
6
+ # @rbs tools: Array<Tool>
7
+ def initialize(tools: [])
8
+ @_tools = tools
9
+ end
10
+
11
+ # @rbs name: String
12
+ def tool?(name)
13
+ @_tools.any? { |tool| tool.name == name }
14
+ end
15
+
16
+ # @rbs name: String
17
+ def tool(name)
18
+ @_tools.find { |tool| tool.name == name }
19
+ end
20
+
21
+ # @rbs memory: Memory?
22
+ def call(**)
23
+ raise NotImplementedError
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autoflux
4
+ # The Assistant state is used to call the agent.
5
+ class Assistant
6
+ OUTPUT_ROLE_NAME = "assistant"
7
+
8
+ def call(workflow:)
9
+ res = workflow.agent.call(memory: workflow.memory)
10
+ workflow.memory.push(res)
11
+ return Tools.new if res["tool_calls"]&.any?
12
+
13
+ workflow.io.write(res["content"]) if res["role"] == OUTPUT_ROLE_NAME
14
+
15
+ User.new
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autoflux
4
+ # The IO is abstract class to represent the interface of the IO
5
+ class IO
6
+ def read
7
+ raise NotImplementedError
8
+ end
9
+
10
+ # @rbs data: String
11
+ def write(_)
12
+ raise NotImplementedError
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autoflux
4
+ # The Memory is a class to store the memory of the workflow.
5
+ class Memory
6
+ attr_reader :data, :verbose
7
+
8
+ # @rbs data: Array[Hash]
9
+ def initialize(data: [], verbose: false)
10
+ @data = data
11
+ @verbose = verbose
12
+ freeze
13
+ end
14
+
15
+ # Push the data to the memory.
16
+ #
17
+ # @rbs data: Hash
18
+ def push(data)
19
+ puts JSON.pretty_generate(data) if verbose
20
+ @data.push(data)
21
+ end
22
+
23
+ # Get the last data from the memory.
24
+ def last
25
+ @data.last
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autoflux
4
+ # The Start state is used to start the workflow.
5
+ class Start < State
6
+ def call(**) = User.new
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autoflux
4
+ # The State is abstract class to represent the interface of the state
5
+ class State
6
+ # @rbs workflow: Workflow
7
+ def call(**)
8
+ raise NotImplementedError
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autoflux
4
+ # The Stdio is a class to represent the standard input/output.
5
+ class Stdio < IO
6
+ attr_accessor :prompt
7
+
8
+ # @rbs input: IO
9
+ # @rbs output: IO
10
+ def initialize(input: $stdin, output: $stdout, prompt: nil)
11
+ super()
12
+
13
+ @input = input
14
+ @output = output
15
+ @prompt = prompt
16
+ freeze
17
+ end
18
+
19
+ def read
20
+ print prompt if prompt
21
+ @input.gets.chomp
22
+ end
23
+
24
+ # @rbs data: String
25
+ def write(data)
26
+ @output.puts(data)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autoflux
4
+ # The Stop state is used to stop the workflow.
5
+ class Stop < State
6
+ def call(**) = nil
7
+ end
8
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autoflux
4
+ # The Tool is a abstract class to represent the adapter of tools which can be used by the Language Model.
5
+ class Tool
6
+ attr_reader :name, :description, :parameters
7
+
8
+ def initialize(name:, description:, parameters: nil)
9
+ @name = name
10
+ @description = description
11
+ @parameters = parameters
12
+ freeze
13
+ end
14
+
15
+ def call(**)
16
+ raise NotImplementedError
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autoflux
4
+ # The Tools state is used to call the tools provided by the agent.
5
+ class Tools
6
+ def call(workflow:)
7
+ workflow.memory.last["tool_calls"]&.each do |tool|
8
+ workflow.memory.push(
9
+ role: :tool,
10
+ content: run(workflow: workflow, tool: tool).to_json,
11
+ tool_call_id: tool["id"]
12
+ )
13
+ end
14
+
15
+ Assistant.new
16
+ end
17
+
18
+ protected
19
+
20
+ def run(workflow:, tool:)
21
+ name = tool.dig("function", "name")
22
+ params = JSON.parse(tool.dig("function", "arguments"), symbolize_names: true)
23
+ return { status: "error", message: "Tool not found" } unless workflow.agent.tool?(name)
24
+
25
+ workflow.agent.tool(name).call(**params)
26
+ rescue JSON::ParserError
27
+ { status: "error", message: "Invalid arguments" }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autoflux
4
+ # The User state is used to get the user input.
5
+ class User
6
+ EXIT_COMMAND = "exit"
7
+
8
+ def call(workflow:)
9
+ input = workflow.io.read
10
+ return Stop.new if input == EXIT_COMMAND
11
+
12
+ workflow.memory.push(role: :user, content: input)
13
+
14
+ Assistant.new
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autoflux
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autoflux
4
+ # The workflow is a state machine to manage the flow of agentic AI.
5
+ class Workflow
6
+ attr_reader :agent, :state, :memory, :io
7
+
8
+ # @rbs state: State
9
+ def initialize(agent:, io:, state: Start.new, memory: Memory.new)
10
+ @agent = agent
11
+ @io = io
12
+ @state = state
13
+ @memory = memory
14
+ end
15
+
16
+ # Run the workflow.
17
+ #
18
+ # @rbs system_prompt: String?
19
+ def run(system_prompt: nil)
20
+ memory.push(role: :system, content: system_prompt) unless system_prompt.nil?
21
+ @state = @state.call(workflow: self) until @state.nil?
22
+ end
23
+ end
24
+ end
data/lib/autoflux.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "autoflux/version"
4
+
5
+ # The Autoflux is a lightweight AI agent framework.
6
+ module Autoflux
7
+ class Error < StandardError; end
8
+
9
+ require_relative "autoflux/tool"
10
+ require_relative "autoflux/agent"
11
+ require_relative "autoflux/io"
12
+ require_relative "autoflux/memory"
13
+ require_relative "autoflux/state"
14
+ require_relative "autoflux/start"
15
+ require_relative "autoflux/user"
16
+ require_relative "autoflux/assistant"
17
+ require_relative "autoflux/tools"
18
+ require_relative "autoflux/stop"
19
+ require_relative "autoflux/workflow"
20
+ end