hellm 0.0.4 → 0.0.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 11522217c18c262d67881d31f50efb136d43fb3f9465c3ed25cbf95659b927ef
4
- data.tar.gz: bf00259cd39073946654e42e8c63a04fe95310fbd94b12d772929c7fc8194b5c
3
+ metadata.gz: 7e145f6843dcffe58f1bcd576ca4c945d2e9727caf765500cf228cbb44d77c9e
4
+ data.tar.gz: 6ce567f6e7d01083a5a9b21b2ef82476b17e1ccbac249c201291b436f15b6055
5
5
  SHA512:
6
- metadata.gz: cf31d07cca0558b43e1fbbcaf338493e268352867e450b1f98cda732f8728fe5685dbb6a10e23ccb749e7b323d59e3673e0bdaace0a2629c623c83013bb2355d
7
- data.tar.gz: 48919c27642fba873c2d4348b933fc899bb3735e42e57590639c058cb835462727c3ac91861754ba279aebf4c82eae7d9e737884c4fbae486130e89534d8fea1
6
+ metadata.gz: 4cfd84c50054e316c6e402f9360599bca016e3f1b8f36db599bd97f1ffe8a4bc951b110c6c462b7e9846a7485fe6394c74c72fac41391795c24e1bdbcf807322
7
+ data.tar.gz: 268ad8d66f1a59e8583a46d09b06efa1114d68c6c9e182b998bb818da6e2a1b24b6519494da34695c5aba6c4d71b95bfd9563b41b9280a27935ee62240480786
data/ARCHITECTURE.md CHANGED
@@ -23,8 +23,8 @@ Main source folders:
23
23
 
24
24
  ```mermaid
25
25
  flowchart TD
26
- A[CLI: bin/hellm] --> B[Hellm.build_agent]
27
- B --> C[Hellm::Builder]
26
+ A[CLI: bin/hellm] --> B[Hellm.open_project]
27
+ B --> C[Hellm::Project]
28
28
  C --> D[Load agents from .hellm/agents]
29
29
  C --> E[Load skills from .hellm/skills/**/SKILL.md]
30
30
  C --> F[Register builtin tools in Registry]
@@ -45,14 +45,14 @@ Location: lib/hellm.rb
45
45
 
46
46
  Responsibilities:
47
47
  - Stores global API key fallback from OPENAI_API_KEY
48
- - Builds an agent through Hellm::Builder
48
+ - Builds an agent through Hellm::Project
49
49
  - Exposes logger and log-level integration with Langchain
50
50
 
51
51
  Design notes:
52
52
  - Global mutable state is limited to logger and API key.
53
53
  - Agent construction delegates to Builder to keep orchestration out of the top-level API.
54
54
 
55
- ### Hellm::Builder
55
+ ### Hellm::Project
56
56
 
57
57
  Location: lib/hellm/builder.rb
58
58
 
@@ -76,7 +76,6 @@ Responsibilities:
76
76
  - Wraps Langchain::Assistant lifecycle and message exchange
77
77
  - Converts configured tool names to tool instances via Registry
78
78
  - Adds text and image input messages
79
- - Streams callbacks through StreamAdapter
80
79
  - Executes with retry logic for transient rate-limit errors
81
80
 
82
81
  Behavior details:
@@ -100,13 +99,13 @@ Location: lib/hellm/skill.rb
100
99
  Responsibilities:
101
100
  - Value object representing skill metadata and markdown content
102
101
 
103
- ### Hellm::StreamAdapter
102
+ ### Hellm::Sesstion
104
103
 
105
- Location: lib/hellm/stream_adapter.rb
104
+ Location: lib/hellm/session.rb
106
105
 
107
106
  Responsibilities:
108
- - Callback interface for streamed assistant messages and tool calls
109
- - Base implementation is intentionally empty and intended for customization
107
+ - Callback interface for streamed assistant messages
108
+ - Base implementation is intended for customization
110
109
 
111
110
  ## Tooling Architecture
112
111
 
@@ -121,7 +120,6 @@ Responsibilities:
121
120
  - Creates and registers sub-agent tools
122
121
 
123
122
  Key implications:
124
- - Tool instances are shared after first creation.
125
123
  - Custom tools should avoid mutable request-specific state.
126
124
 
127
125
  ### Hellm::Tools::SkillTool
@@ -178,10 +176,10 @@ Project configuration is stored in a `.hellm` directory with the following struc
178
176
  ## Extension Points
179
177
 
180
178
  Supported extension seams:
181
- - Register additional tools via Hellm::Tools::Registry.instance.add
179
+ - Register additional tools via Hellm::Tools::Registry#add
182
180
  - Add new agents by placing markdown files in .hellm/agents
183
181
  - Add new skills by placing SKILL.md files in .hellm/skills
184
- - Provide a custom StreamAdapter implementation for telemetry or UI streaming
182
+ - Provide a custom Session implementation for telemetry/UI streaming/restorability/resumeability
185
183
 
186
184
  ## Known Risks And Gaps
187
185
 
data/README.md CHANGED
@@ -51,8 +51,8 @@ The framework provides the following tools for agents to use:
51
51
  | ------------- | ----------------------- | ------------------------------------------------------- |
52
52
  | `calculator` | | a simple calculator for basic arithmetic operations. |
53
53
  | `webfetch` | `web` | fetches web pages or other content from URLs. |
54
- | `websearch` | `web` | performs web searches and returns search results. |
55
- | `web` | | combines web fetching and web searching capabilities. |
54
+ | `websearch` | `web` | performs web searches and returns search results. utilizes [Brave Search API](https://brave.com/search/api/), which needs environment variable BRAVE_API_KEY to be set to a valid API key. |
55
+ | `web` | | combines `webfetch` and `websearch` capabilities. |
56
56
 
57
57
 
58
58
  ### Custom Tools
@@ -60,7 +60,9 @@ The framework provides the following tools for agents to use:
60
60
  Custom tools can be defined using `Langchain::ToolDefinition`, like it is described in the [Langchain documentation](https://github.com/patterns-ai-core/langchainrb#creating-custom-tools). Such tools need to be registered in `Hellm` before they can be used by agents:
61
61
 
62
62
  ```ruby
63
- Hellm::Tools::Registry.instance.add(:calculator) do
63
+ project = Hellm.open_project("/path/to/project")
64
+
65
+ project.registry.add(:calculator) do
64
66
  Langchain::Tool::Calculator.new
65
67
  end
66
68
  ```
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.4
1
+ 0.0.5
data/lib/hellm/agent.rb CHANGED
@@ -1,13 +1,10 @@
1
- require_relative "stream_adapter"
1
+ require_relative "session"
2
2
  require_relative "tools/registry"
3
3
  require "base64"
4
4
  require "langchain"
5
5
 
6
6
  class Hellm::Agent
7
- attr_accessor :name, :llm, :description, :agents
8
-
9
- # @param stream_adapter [Hellm::StreamAdapter] an object that responds to `message` and `tool_call` for streaming messages and tool calls
10
- attr_accessor :stream_adapter
7
+ attr_accessor :name, :llm, :description, :agents, :registry
11
8
 
12
9
  # instructions (string) for the agent.
13
10
  attr_accessor :instructions
@@ -28,7 +25,7 @@ class Hellm::Agent
28
25
  #
29
26
  attr_accessor :tools
30
27
 
31
- def initialize(model: nil, name: nil, description: nil, stream_adapter: nil, openai_api_key: nil, agents: nil, tools: nil, instructions: nil)
28
+ def initialize(model: nil, name: nil, description: nil, session: nil, openai_api_key: nil, agents: nil, tools: nil, instructions: nil, registry: nil)
32
29
  model ||= "gpt-5-nano"
33
30
  Hellm.logger.debug("Initializing agent #{name} (#{model})")
34
31
  @llm = Langchain::LLM::OpenAI.new(
@@ -38,9 +35,11 @@ class Hellm::Agent
38
35
  self.name = name || self.class.name
39
36
  self.description = description || ""
40
37
  self.instructions = instructions || ""
41
- self.stream_adapter = stream_adapter || Hellm::StreamAdapter.new
38
+ self.session = session || Hellm::Session.new
42
39
  self.agents = agents || []
43
40
  self.tools = tools || []
41
+ self.registry = registry || Hellm::Tools::Registry.new
42
+ @replaying_session = false
44
43
  end
45
44
 
46
45
 
@@ -64,15 +63,40 @@ class Hellm::Agent
64
63
  end
65
64
 
66
65
  def new_session
66
+ @session = nil
67
67
  @assistant = nil
68
68
  end
69
69
 
70
+ def session=(value)
71
+ @session = value
72
+ @replaying_session = true
73
+ @session.messages.each do |message|
74
+ attributes = {
75
+ role: fetch_indifferent(message, :role),
76
+ content: fetch_indifferent(message, :content),
77
+ image_url: fetch_indifferent(message, :image_url),
78
+ tool_calls: fetch_indifferent(message, :tool_calls),
79
+ tool_call_id: fetch_indifferent(message, :tool_call_id)
80
+ }
81
+ assistant.add_message(**attributes)
82
+ end
83
+ @replaying_session = false
84
+ end
85
+
86
+ def session
87
+ @session ||= Hellm::Session.new
88
+ end
89
+
70
90
  protected
91
+ def fetch_indifferent(hash, key)
92
+ hash[key.to_sym] || hash[key.to_s]
93
+ end
94
+
71
95
  def assistant
72
96
  if @assistant.nil?
73
97
  tool_instances = [
74
- *Hellm::Tools::Registry.instance.get(*tools),
75
- *Hellm::Tools::Registry.instance.get_agent_tools(*agents)
98
+ *self.registry.get(*tools),
99
+ *self.registry.get_agent_tools(*agents)
76
100
  ]
77
101
  @assistant = Langchain::Assistant.new(
78
102
  llm: llm,
@@ -81,24 +105,24 @@ class Hellm::Agent
81
105
  )
82
106
  @assistant.add_message_callback = ->(message) {
83
107
  Hellm.logger.debug("[Agent #{name} message] #{message.inspect}")
84
- self.stream_adapter.message(message)
108
+ if @replaying_session != true
109
+ self.session.add_message({
110
+ agent: name,
111
+ role: message.role,
112
+ content: message.content,
113
+ image_url: message.image_url,
114
+ tool_calls: message.tool_calls,
115
+ tool_call_id: message.tool_call_id
116
+ })
117
+ end
85
118
  }
86
119
  @assistant.tool_execution_callback = ->(call_id, name, method, args) {
87
120
  Hellm.logger.debug("[Agent #{self.name} tool_call] call_id: #{call_id}, name: #{name}, method: #{method}, args: #{args}")
88
- self.stream_adapter.tool_call(call_id, name, method, args)
89
121
  }
90
122
  end
91
123
  @assistant
92
124
  end
93
125
 
94
- def build_instruction_preface(agent_tool)
95
- preamble = "# Hints\n\n"
96
- preamble = "#{preamble}You are an AI agent named #{name}. You have access to the following skills and sub-agents:\n\n"
97
- preamble << agent_tool.instructions_for_agents(*agents)
98
- preamble << "\n\n"
99
- preamble
100
- end
101
-
102
126
  def normalize_image_input(image_input)
103
127
  return image_input if image_input.start_with?("http://", "https://", "data:image/")
104
128
 
@@ -1,56 +1,61 @@
1
1
  require_relative "markdown_file"
2
- require_relative "stream_adapter"
2
+ require_relative "session"
3
3
  require_relative "agent"
4
4
  require_relative "skill"
5
5
  require_relative "tools/skill_tool"
6
6
  require_relative "tools/web_fetch_tool"
7
7
  require_relative "tools/brave_search_tool"
8
+ require_relative "tools/registry"
8
9
 
9
- class Hellm::Builder
10
- attr_accessor :project_dir, :hellm_dir, :stream_adapter
10
+ class Hellm::Project
11
+ attr_accessor :project_dir, :hellm_dir, :agents, :registry
11
12
 
12
13
  SUPPORTED_SKILL_ATTRIBUTEES = ["name", "description", "argument_hint"]
13
14
  SUPPORTED_AGENT_ATTRIBUTEES = ["model", "name", "description", "tools", "agents"]
14
15
 
15
- def initialize(project_dir: nil, stream_adapter: nil, openai_api_key: nil)
16
- @project_dir = project_dir
17
- @hellm_dir = File.join(project_dir, ".hellm")
18
- @hellm_dir = File.join(project_dir, ".github") if !Dir.exist?(@hellm_dir)
19
- raise ArgumentError, "not found: #{project_dir}/.hellm" unless Dir.exist?(@hellm_dir)
16
+ def initialize(project_dir: nil, openai_api_key: nil)
17
+ self.registry = Hellm::Tools::Registry.new
18
+ self.project_dir = project_dir
19
+ self.hellm_dir = File.join(project_dir, ".hellm")
20
+ self.hellm_dir = File.join(project_dir, ".github") if !Dir.exist?(self.hellm_dir)
21
+ raise ArgumentError, "not found: #{project_dir}/.hellm" unless Dir.exist?(self.hellm_dir)
20
22
 
21
- @stream_adapter = stream_adapter
22
23
  @openai_api_key = openai_api_key
23
-
24
24
  add_builtin_tools_to_registry
25
+ self.agents = build_project
25
26
  end
26
27
 
27
- def build(talking_to: nil)
28
- agents = get_agents_from_project_dir
29
- skills = get_skills_from_project_dir
30
- skill_tool = Hellm::Tools::SkillTool.new
31
- skill_tool.add(*skills)
32
- Hellm::Tools::Registry.instance.add(:skill, skill_tool)
33
- Hellm::Tools::Registry.instance.add_agents(*agents)
34
-
35
- if agents.empty?
28
+ def find_agent(name)
29
+ if self.agents.empty?
36
30
  agent = Hellm::Agent.new
37
31
  else
38
- agent = agents.find{ |agent| agent.name == talking_to }
39
- raise ArgumentError, "Agent not found: #{talking_to}" if agent.nil?
32
+ agent = self.agents.find{ |agent| agent.name == name }
33
+ raise ArgumentError, "Agent not found: #{name}" if agent.nil?
40
34
  end
41
35
  agent
42
36
  end
43
37
 
44
38
  private
39
+ def build_project
40
+ skills = build_skills_from_project_dir
41
+ skill_tool = Hellm::Tools::SkillTool.new
42
+ skill_tool.add(*skills)
43
+ self.registry.add(:skill, skill_tool)
44
+
45
+ agents = build_agents_from_project_dir(skill_tool)
46
+ self.registry.add_agents(*agents)
47
+ agents
48
+ end
49
+
45
50
  def add_builtin_tools_to_registry
46
- Hellm::Tools::Registry.instance.add(:calculator) { Langchain::Tool::Calculator.new }
47
- Hellm::Tools::Registry.instance.add(:bravesearch) { Hellm::Tools::BraveSearchTool.new }
48
- Hellm::Tools::Registry.instance.add(:webfetch) { Hellm::Tools::WebFetchTool.new }
51
+ self.registry.add(:calculator) { Langchain::Tool::Calculator.new }
52
+ self.registry.add(:bravesearch) { Hellm::Tools::BraveSearchTool.new }
53
+ self.registry.add(:webfetch) { Hellm::Tools::WebFetchTool.new }
49
54
  end
50
55
 
51
- def get_skills_from_project_dir
56
+ def build_skills_from_project_dir
52
57
  skills_dir = File.join(hellm_dir, "skills")
53
- return unless Dir.exist?(skills_dir)
58
+ return [] unless Dir.exist?(skills_dir)
54
59
 
55
60
  Dir.glob(File.join(skills_dir, "**/SKILL.md")).map do |file|
56
61
  markdown_dir = File.dirname(file)
@@ -65,9 +70,9 @@ class Hellm::Builder
65
70
  end
66
71
  end
67
72
 
68
- def get_agents_from_project_dir
73
+ def build_agents_from_project_dir(skill_tool)
69
74
  agents_dir = File.join(hellm_dir, "agents")
70
- return unless Dir.exist?(agents_dir)
75
+ return [] unless Dir.exist?(agents_dir)
71
76
 
72
77
  Dir.glob(File.join(agents_dir, "*.md")).map do |file|
73
78
  markdown_file = Hellm::MarkdownFile.read(file)
@@ -82,8 +87,8 @@ class Hellm::Builder
82
87
  tools: tools,
83
88
  agents: markdown_file.fetch("agents", []),
84
89
  instructions: markdown_file.content.strip,
85
- stream_adapter: stream_adapter,
86
- openai_api_key: @openai_api_key
90
+ openai_api_key: @openai_api_key,
91
+ registry: self.registry
87
92
  )
88
93
  end
89
94
  end
@@ -0,0 +1,17 @@
1
+ class Hellm::Session
2
+ def initialize(messages = [])
3
+ @messages = messages
4
+ end
5
+
6
+ def messages
7
+ @messages
8
+ end
9
+
10
+ def messages=(value)
11
+ @messages = value
12
+ end
13
+
14
+ def add_message(message)
15
+ @messages << message
16
+ end
17
+ end
@@ -1,32 +1,31 @@
1
1
  module Hellm::Tools
2
2
  require "langchain"
3
3
 
4
+
4
5
  class AgentToolFactory
5
- def self.create_sub_agent_tool(agent_name, agent)
6
- klass = Class.new do
7
- @@agent_name = agent_name
6
+ class AgentBase
7
+ extend Langchain::ToolDefinition
8
8
 
9
- extend Langchain::ToolDefinition
9
+ def initialize(agent)
10
+ @agent = agent
11
+ end
10
12
 
11
- def self.tool_name
12
- @@agent_name
13
- end
13
+ def run_agent(content: nil, image: nil)
14
+ @agent.new_session
15
+ @agent.add(content: content, image: image)
16
+ @agent.run
17
+ @agent.response
18
+ end
19
+ end
20
+
21
+ def self.create_sub_agent_tool(agent_name, agent)
22
+ klass = Class.new(AgentBase) do
23
+ define_singleton_method(:tool_name) { agent_name }
14
24
 
15
25
  define_function "run_agent", description: "Run the agent #{agent.name} and return its response.\n#{agent.description}" do
16
26
  property :content, type: "string", description: "Textual content to pass to the agent.", required: false
17
27
  property :image, type: "string", description: "Image URL or path to pass to the agent.", required: false
18
28
  end
19
-
20
- def initialize(agent)
21
- @agent = agent
22
- end
23
-
24
- def run_agent(content: nil, image: nil)
25
- @agent.new_session
26
- @agent.add(content: content, image: image)
27
- @agent.run
28
- @agent.response
29
- end
30
29
  end
31
30
  klass.new(agent)
32
31
  end
@@ -3,20 +3,18 @@ require_relative "agent_tool_factory"
3
3
  module Hellm::Tools
4
4
  class Registry
5
5
  ALIASES = {
6
- "default" => ["skill"],
6
+ "default" => ["skill", "calculator"],
7
7
  "web" => ["webfetch", "bravesearch"],
8
8
  "websearch" => ["bravesearch"],
9
9
  "agent" => [] # agent tools are dynamically generated from registered agents
10
10
  }
11
11
 
12
-
13
- def self.instance
14
- @instance ||= new
15
- end
16
-
17
12
  def initialize
18
13
  @tools = {}
19
- @agent_tools = {}
14
+ end
15
+
16
+ def available_tools
17
+ @tools.keys
20
18
  end
21
19
 
22
20
  def add(tool_name, instance=nil, &factory)
@@ -4,6 +4,12 @@ module Hellm::Tools
4
4
  class SkillTool
5
5
  extend Langchain::ToolDefinition
6
6
 
7
+ attr_reader :skills
8
+
9
+ def initialize
10
+ @skills = []
11
+ end
12
+
7
13
  def add(*skills)
8
14
  skills.each do |skill|
9
15
  add_skill(skill)
@@ -12,6 +18,7 @@ module Hellm::Tools
12
18
 
13
19
  private
14
20
  def add_skill(skill)
21
+ @skills << skill
15
22
  skill_name = skill.name.gsub(/[\s-]+/, "_").downcase
16
23
  method_name = "get_skill_#{skill_name}"
17
24
  self.class.class_eval(
data/lib/hellm.rb CHANGED
@@ -1,11 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "hellm/version"
4
- require_relative "hellm/builder"
4
+ require_relative "hellm/project"
5
5
 
6
6
  module Hellm
7
7
  class Error < StandardError; end
8
- # Your code goes here...
9
8
 
10
9
  def self.openai_api_key=(key)
11
10
  @api_key = key if !key.nil? && !key.empty?
@@ -15,10 +14,9 @@ module Hellm
15
14
  @api_key ||= ENV["OPENAI_API_KEY"]
16
15
  end
17
16
 
18
- def self.build_agent(agent_name, project_dir, stream_adapter: nil, openai_api_key: nil)
17
+ def self.open_project(project_dir, openai_api_key: nil)
19
18
  self.openai_api_key = openai_api_key
20
- builder = Hellm::Builder.new(project_dir: project_dir, stream_adapter: stream_adapter, openai_api_key: openai_api_key)
21
- builder.build(talking_to: agent_name)
19
+ Hellm::Project.new(project_dir: project_dir, openai_api_key: openai_api_key)
22
20
  end
23
21
 
24
22
  def self.logger=(logger)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hellm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - ubity UG
@@ -123,10 +123,10 @@ files:
123
123
  - lib/brave/search_api/client.rb
124
124
  - lib/hellm.rb
125
125
  - lib/hellm/agent.rb
126
- - lib/hellm/builder.rb
127
126
  - lib/hellm/markdown_file.rb
127
+ - lib/hellm/project.rb
128
+ - lib/hellm/session.rb
128
129
  - lib/hellm/skill.rb
129
- - lib/hellm/stream_adapter.rb
130
130
  - lib/hellm/tools/agent_tool_factory.rb
131
131
  - lib/hellm/tools/brave_search_tool.rb
132
132
  - lib/hellm/tools/registry.rb
@@ -1,7 +0,0 @@
1
- class Hellm::StreamAdapter
2
- def message(message)
3
- end
4
-
5
- def tool_call(call_id, name, method, args)
6
- end
7
- end