ollama_chat 0.0.58 → 0.0.59

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: e977681ee5ceb8267b4c2d9f2ad7f9c22ac168791a14cf12a6ae5d8696d58c24
4
- data.tar.gz: f5277d49ba8ccd5af55f83e742dfcb5b9818395151478547f183873669024d09
3
+ metadata.gz: 98fa6fed72170e4aa21b04c8a66ba443942ed66cb9b2eb380708029a2fd592f5
4
+ data.tar.gz: 40ed71dcd2a5770289dc1d1e5e29f44679b436584b8ce24ed6fa4756c8c694f6
5
5
  SHA512:
6
- metadata.gz: c39142b8c05b16591d285f66d80dbbda12e20058a8fb5b533e219c029bc08e66ade15e5c87a90228a9598929f0444e71b1fdf6cfe18abb4426a423f177716086
7
- data.tar.gz: 734d78b497080fce515f6fa326396cb031f0540b18d565cdd324eeb4f1d16010840afd1177c55ac0cf24f7dc49b7101d108d17cf499de5d75b1daf54559ef7a3
6
+ metadata.gz: b64ad829de42272883cfdfad370cc2a34a97d4b68efe957019a05f07e8b6b8ff0aa1a3d80711427da117c58bfbb4d39265921bcfe228bbac7805a7c4fa1a8725
7
+ data.tar.gz: 3d91f987a19b7b387ca2f21edb7e0b3643c79e7f5c983c160d441a08415b47c5267e74f86f1bd30f3881c5001b4916fd9555d461f733b3f9d0278fb0e0b9a895
data/CHANGES.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-02-05 v0.0.59
4
+
5
+ ### New Features
6
+
7
+ - **Dynamic Tool Management**: Added `/tools` command with subcommands `/tools
8
+ enable` and `/tools disable` for listing, enabling, or disabling tools
9
+ dynamically
10
+
11
+ - **Three Built-in Tools**:
12
+ - `get_current_weather` - Fetches real-time temperature from German Weather
13
+ Service (DWD)
14
+ - `get_cve` - Retrieves CVE details from MITRE API
15
+ - `get_endoflife` - Queries endoflife.date API for software lifecycle
16
+ information
17
+
18
+ ### Technical Improvements
19
+
20
+ - **Tool Calling System**: Implemented comprehensive tool management using
21
+ `@enabled_tools` array
22
+ - **Configuration Support**: Added tool configurations in `default_config.yml`
23
+ - **Integration**: Enhanced `Ollama::Chat` with proper tool calling integration
24
+ and `Ollama::Tool` support
25
+ - **Error Handling**: Robust error handling for external API calls with proper
26
+ fallbacks
27
+
28
+ ### Documentation & Testing
29
+
30
+ - Updated README with new tool management commands
31
+ - Added comprehensive RSpec tests for all three new tools
32
+ - Enhanced gemspec with updated file listings and dependencies
33
+ - Implemented caching for HTTP responses using
34
+ `OllamaChat::Utils::CacheFetcher`
35
+
36
+ ### Code Structure
37
+
38
+ - **New Modules**: `OllamaChat::ToolCalling`, `OllamaChat::Tools::CVE`,
39
+ `OllamaChat::Tools::EndOfLife`, `OllamaChat::Tools::Weather`
40
+ - **Enhanced Classes**: `OllamaChat::Chat` with tool management and `DWDSensor`
41
+ for weather data retrieval
42
+ - **Dependency**: Added `rubyzip` for DWD data processing
43
+
44
+ This release transforms OllamaChat from a chat interface into a smart assistant
45
+ capable of executing external tools and accessing real-world data.
46
+
3
47
  ## 2026-02-02 v0.0.58
4
48
 
5
49
  - Updated Redis image to version to valkey **9.0.1** in docker-compose.yml
data/README.md CHANGED
@@ -187,6 +187,7 @@ The following commands can be given inside the chat, if prefixed by a `/`:
187
187
  /revise_last edit the last response in an external editor
188
188
  /output filename save last response to filename
189
189
  /pipe command write last response to command's stdin
190
+ /tools [enable|disable] list enabled, enable, or disable tools
190
191
  /vim insert the last message into a vim server
191
192
  /quit to quit
192
193
  /help to view this help
data/Rakefile CHANGED
@@ -59,6 +59,7 @@ GemHadar do
59
59
  dependency 'csv', '~> 3.0'
60
60
  dependency 'const_conf', '~> 0.3'
61
61
  dependency 'context_spook', '~> 1.5'
62
+ dependency 'rubyzip', '~> 3.0'
62
63
  development_dependency 'all_images', '~> 0.6'
63
64
  development_dependency 'rspec', '~> 3.2'
64
65
  development_dependency 'kramdown', '~> 2.0'
@@ -53,6 +53,7 @@ class OllamaChat::Chat
53
53
  include OllamaChat::Conversation
54
54
  include OllamaChat::InputContent
55
55
  include OllamaChat::MessageEditing
56
+ include OllamaChat::ToolCalling
56
57
 
57
58
  # Initializes a new OllamaChat::Chat instance with the given command-line
58
59
  # arguments.
@@ -112,6 +113,8 @@ class OllamaChat::Chat
112
113
  @kramdown_ansi_styles = configure_kramdown_ansi_styles
113
114
  init_chat_history
114
115
  @opts[?S] and init_server_socket
116
+ @enabled_tools = []
117
+ @tool_call_results = {}
115
118
  rescue ComplexConfig::AttributeMissing, ComplexConfig::ConfigurationSyntaxError => e
116
119
  fix_config(e)
117
120
  end
@@ -372,6 +375,16 @@ class OllamaChat::Chat
372
375
  STDERR.puts "Warning: No message found to insert into Vim"
373
376
  end
374
377
  :next
378
+ when %r(^/tools(?:\s+(enable|disable))?$)
379
+ case $1
380
+ when nil
381
+ list_tools
382
+ when 'enable'
383
+ enable_tool
384
+ when 'disable'
385
+ disable_tool
386
+ end
387
+ :next
375
388
  when %r(^/config$)
376
389
  display_config
377
390
  :next
@@ -563,16 +576,20 @@ class OllamaChat::Chat
563
576
  type = :terminal_input
564
577
  input_prompt = bold { color(172) { message_type(@images) + " user" } } + bold { "> " }
565
578
  begin
566
- content = enable_command_completion do
567
- if prefill_prompt = @prefill_prompt.full?
568
- Reline.pre_input_hook = -> {
569
- Reline.insert_text prefill_prompt.gsub(/\n*\z/, '')
570
- @prefill_prompt = nil
571
- }
572
- else
573
- Reline.pre_input_hook = nil
579
+ if content = handle_tool_call_results?
580
+ @parse_content = false
581
+ else
582
+ content = enable_command_completion do
583
+ if prefill_prompt = @prefill_prompt.full?
584
+ Reline.pre_input_hook = -> {
585
+ Reline.insert_text prefill_prompt.gsub(/\n*\z/, '')
586
+ @prefill_prompt = nil
587
+ }
588
+ else
589
+ Reline.pre_input_hook = nil
590
+ end
591
+ Reline.readline(input_prompt, true)&.chomp
574
592
  end
575
- Reline.readline(input_prompt, true)&.chomp
576
593
  end
577
594
  rescue Interrupt
578
595
  if message = server_socket_message
@@ -635,6 +652,7 @@ class OllamaChat::Chat
635
652
  options: @model_options,
636
653
  stream: stream.on?,
637
654
  think: ,
655
+ tools: ,
638
656
  &handler
639
657
  )
640
658
  rescue Ollama::Errors::BadRequestError
@@ -9,7 +9,7 @@
9
9
  #
10
10
  # @example Processing a chat response
11
11
  # follow_chat = OllamaChat::FollowChat.new(chat: chat_instance, messages: message_list)
12
- # follow_chat.call(response)
12
+ # follow_chat.tool_call(response)
13
13
  class OllamaChat::FollowChat
14
14
  include Ollama
15
15
  include Ollama::Handlers::Concern
@@ -80,11 +80,33 @@ class OllamaChat::FollowChat
80
80
 
81
81
  output_eval_stats(response)
82
82
 
83
+ handle_tool_calls(response)
84
+
83
85
  self
84
86
  end
85
87
 
86
88
  private
87
89
 
90
+ # The handle_tool_calls method processes tool calls from a response and
91
+ # executes them.
92
+ #
93
+ # This method checks if the response contains tool calls, and if so, iterates
94
+ # through each tool call to execute the corresponding tool from the
95
+ # registered tools. The results of the tool execution are stored in the
96
+ # chat's tool_call_results hash using the tool name as the key.
97
+ #
98
+ # @param response [Object] the response object containing tool calls to
99
+ # process
100
+ def handle_tool_calls(response)
101
+ return unless response.message.ask_and_send(:tool_calls)
102
+
103
+ response.message.tool_calls.each do |tool_call|
104
+ name = tool_call.function.name
105
+ @chat.tool_call_results[name] = OllamaChat::Tools.registered[name].
106
+ execute(tool_call, config: @chat.config)
107
+ end
108
+ end
109
+
88
110
  # The truncate_for_terminal method processes text to fit within a specified
89
111
  # number of lines.
90
112
  #
@@ -152,6 +152,7 @@ module OllamaChat::Information
152
152
  /revise_last edit the last response in an external editor
153
153
  /output filename save last response to filename
154
154
  /pipe command write last response to command's stdin
155
+ /tools [enable|disable] list enabled, enable, or disable tools
155
156
  /vim insert the last message into a vim server
156
157
  /quit to quit
157
158
  /help to view this help
@@ -84,3 +84,10 @@ web_search:
84
84
  url: <%= OllamaChat::EnvConfig::OLLAMA::SEARXNG_URL %>
85
85
  vim:
86
86
  clientserver: socket
87
+ tools:
88
+ get_current_weather:
89
+ station_id: '00433'
90
+ get_cve:
91
+ url: 'https://cveawg.mitre.org/api/cve/%{cve_id}'
92
+ get_endoflife:
93
+ url: "https://endoflife.date/api/v1/products/%{product}"
@@ -0,0 +1,104 @@
1
+ # A module that provides tool calling functionality for OllamaChat.
2
+ #
3
+ # The ToolCalling module encapsulates methods for managing and processing tool
4
+ # calls within the chat application. It handles the registration and execution
5
+ # of tools that can be invoked during conversations, allowing the chat to
6
+ # interact with external systems or perform specialized tasks beyond simple
7
+ # text generation.
8
+ module OllamaChat::ToolCalling
9
+ # The tools reader returns the registered tools for the chat session.
10
+ #
11
+ # @return [ Hash ] a hash containing the registered tools
12
+ def tools
13
+ @enabled_tools.map { OllamaChat::Tools.registered[it]&.to_hash }.compact
14
+ end
15
+
16
+ # The configured_tools method returns an array of tool names configured for
17
+ # the chat session.
18
+ #
19
+ # This method retrieves the list of available tools from the configuration
20
+ # and returns them as a sorted array of strings. It handles cases where the
21
+ # tools configuration might be nil or empty by returning an empty array.
22
+ #
23
+ # @return [Array<String>] a sorted array of tool names configured for the
24
+ # chat session
25
+ def configured_tools
26
+ Array(config.tools&.attribute_names&.map(&:to_s)).sort
27
+ end
28
+
29
+ # The tool_call_results reader returns the tools' results for the
30
+ # chat session if any.
31
+ #
32
+ # @return [ Hash ] a hash containing the registered tool results
33
+ attr_reader :tool_call_results
34
+
35
+ # The list_tools method displays the sorted list of enabled tools.
36
+ #
37
+ # This method outputs to standard output the alphabetically sorted list of
38
+ # tool names that are currently enabled in the chat session.
39
+ def list_tools
40
+ puts @enabled_tools.sort
41
+ end
42
+
43
+ # The enable_tool method allows the user to select and enable a tool from a
44
+ # list of available tools.
45
+ #
46
+ # This method presents a menu of tools that can be enabled, excluding those
47
+ # that are already enabled. It uses the chooser to display the available
48
+ # tools and handles the user's selection by adding the chosen tool to the
49
+ # list of enabled tools and sorting the list.
50
+ def enable_tool
51
+ select_tools = configured_tools - @enabled_tools
52
+ select_tools += [ '[EXIT]' ]
53
+ case chosen = OllamaChat::Utils::Chooser.choose(select_tools)
54
+ when '[EXIT]', nil
55
+ STDOUT.puts "Exiting chooser."
56
+ return
57
+ when *select_tools
58
+ @enabled_tools << chosen
59
+ @enabled_tools.sort!
60
+ puts "Enabled tool %s" % bold(chosen)
61
+ end
62
+ end
63
+
64
+ # The disable_tool method allows the user to select and disable a tool from a
65
+ # list of enabled tools.
66
+ #
67
+ # This method presents a menu of currently enabled tools to the user,
68
+ # allowing them to choose which tool to disable. It uses the chooser to
69
+ # display the available tools and handles the user's selection by removing
70
+ # the chosen tool from the list of enabled tools and sorting the list
71
+ # afterwards.
72
+ def disable_tool
73
+ select_tools = @enabled_tools
74
+ select_tools += [ '[EXIT]' ]
75
+ case chosen = OllamaChat::Utils::Chooser.choose(select_tools)
76
+ when '[EXIT]', nil
77
+ STDOUT.puts "Exiting chooser."
78
+ return
79
+ when *select_tools
80
+ @enabled_tools.delete chosen
81
+ puts "Disabled tool %s" % bold(chosen)
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ # The handle_tool_call_results? method processes and returns results from
88
+ # tool calls.
89
+ #
90
+ # This method checks if there are any pending tool call results and formats
91
+ # them into a string message. It clears the tool call results after
92
+ # processing.
93
+ #
94
+ # @return [ String, nil ] a formatted string containing tool call results or
95
+ # nil if no results exist
96
+ def handle_tool_call_results?
97
+ @tool_call_results.present? or return
98
+ content = @tool_call_results.map do |name, result|
99
+ "Tool %s returned %s" % [ name, result ]
100
+ end.join(?\n)
101
+ @tool_call_results.clear
102
+ content
103
+ end
104
+ end
@@ -0,0 +1,92 @@
1
+ # A tool for fetching CVE (Common Vulnerabilities and Exposures) information.
2
+ #
3
+ # This tool allows the chat client to retrieve CVE details by ID from a configured
4
+ # API endpoint. It integrates with the Ollama tool calling system to provide
5
+ # security-related information to the language model.
6
+ #
7
+ # @example Using the CVE tool
8
+ # # The tool can be invoked with a CVE ID
9
+ # { "name": "get_cve", "arguments": { "cve_id": "CVE-2023-12345" } }
10
+ #
11
+ # @see OllamaChat::Tools
12
+ class OllamaChat::Tools::CVE
13
+ include Ollama
14
+
15
+ # Initializes a new CVE tool instance.
16
+ #
17
+ # @return [OllamaChat::Tools::CVE] a new CVE tool instance
18
+ def initialize
19
+ @name = 'get_cve'
20
+ end
21
+
22
+ # Returns the name of the tool.
23
+ #
24
+ # @return [String] the name of the tool ('get_cve')
25
+ attr_reader :name
26
+
27
+ # Creates and returns a tool definition for getting CVE information.
28
+ #
29
+ # This method constructs the function signature that describes what the tool
30
+ # does, its parameters, and required fields. The tool expects a CVE-ID
31
+ # parameter to be provided.
32
+ #
33
+ # @return [Ollama::Tool] a tool definition for retrieving CVE information
34
+ def tool
35
+ Tool.new(
36
+ type: 'function',
37
+ function: Tool::Function.new(
38
+ name:,
39
+ description: 'Get the CVE for id as JSON',
40
+ parameters: Tool::Function::Parameters.new(
41
+ type: 'object',
42
+ properties: {
43
+ cve_id: Tool::Function::Parameters::Property.new(
44
+ type: 'string',
45
+ description: 'The CVE-ID to get'
46
+ ),
47
+ },
48
+ required: %w[cve_id]
49
+ )
50
+ )
51
+ )
52
+ end
53
+
54
+ # Executes the CVE lookup operation.
55
+ #
56
+ # This method fetches CVE data from the configured API endpoint using the
57
+ # provided CVE ID. It handles the HTTP request, parses the JSON response,
58
+ # and returns the structured data.
59
+ #
60
+ # @param tool_call [Ollama::Tool::Call] the tool call object containing function details
61
+ # @param opts [Hash] additional options
62
+ # @option opts [ComplexConfig::Settings] :config the configuration object
63
+ # @return [Hash, String] the parsed CVE data as a hash or an error message
64
+ # @raise [StandardError] if there's an issue with the HTTP request or JSON parsing
65
+ def execute(tool_call, **opts)
66
+ config = opts[:config]
67
+ cve_id = tool_call.function.arguments.cve_id
68
+ url = config.tools.get_cve.url % { cve_id: }
69
+ OllamaChat::Utils::Fetcher.get(
70
+ url,
71
+ headers: {
72
+ 'Accept' => 'application/json',
73
+ },
74
+ debug: OllamaChat::EnvConfig::OLLAMA::CHAT::DEBUG
75
+ ) do |tmp|
76
+ data = JSON.parse(tmp.read, object_class: JSON::GenericObject)
77
+ return data
78
+ end
79
+ rescue StandardError => e
80
+ "Failed to fetch CVE for #{cve_id} #{e.class}: #{e.message}"
81
+ end
82
+
83
+ # Converts the tool to a hash representation.
84
+ #
85
+ # This method provides a standardized way to serialize the tool definition
86
+ # for use in tool calling systems.
87
+ #
88
+ # @return [Hash] a hash representation of the tool
89
+ def to_hash
90
+ tool.to_hash
91
+ end
92
+ end
@@ -0,0 +1,98 @@
1
+ # A tool for fetching endoflife.date product information.
2
+ #
3
+ # This tool allows the chat client to retrieve endoflife.date information
4
+ # for software products by ID. It integrates with the Ollama tool calling
5
+ # system to provide lifecycle and support information to the language model.
6
+ #
7
+ # @example Using the endoflife tool
8
+ # # The tool can be invoked with a product name
9
+ # { "name": "get_endoflife", "arguments": { "product": "mysql" } }
10
+ #
11
+ # @see OllamaChat::Tools
12
+ class OllamaChat::Tools::EndOfLife
13
+ include Ollama
14
+
15
+ # Initializes a new endoflife tool instance.
16
+ #
17
+ # @return [OllamaChat::Tools::EndOfLife] a new endoflife tool instance
18
+ def initialize
19
+ @name = 'get_endoflife'
20
+ end
21
+
22
+ # Returns the name of the tool.
23
+ #
24
+ # @return [String] the name of the tool ('get_endoflife')
25
+ attr_reader :name
26
+
27
+ # Creates and returns a tool definition for getting endoflife information.
28
+ #
29
+ # This method constructs the function signature that describes what the tool
30
+ # does, its parameters, and required fields. The tool expects a product name
31
+ # parameter to be provided.
32
+ #
33
+ # @return [Ollama::Tool] a tool definition for retrieving endoflife information
34
+ def tool
35
+ Tool.new(
36
+ type: 'function',
37
+ function: Tool::Function.new(
38
+ name:,
39
+ description: 'Get the endoflife information for a product as JSON',
40
+ parameters: Tool::Function::Parameters.new(
41
+ type: 'object',
42
+ properties: {
43
+ product: Tool::Function::Parameters::Property.new(
44
+ type: 'string',
45
+ description: 'The product name to get endoflife information for'
46
+ ),
47
+ },
48
+ required: %w[product]
49
+ )
50
+ )
51
+ )
52
+ end
53
+
54
+ # Executes the endoflife lookup operation.
55
+ #
56
+ # This method fetches endoflife data from the endoflife.date API using the
57
+ # provided product name. It handles the HTTP request, parses the JSON response,
58
+ # and returns the structured data.
59
+ #
60
+ # @param tool_call [Ollama::Tool::Call] the tool call object containing function details
61
+ # @param opts [Hash] additional options
62
+ # @option opts [ComplexConfig::Settings] :config the configuration object
63
+ # @return [Hash, String] the parsed endoflife data as a hash or an error message
64
+ # @raise [StandardError] if there's an issue with the HTTP request or JSON parsing
65
+ def execute(tool_call, **opts)
66
+ config = opts[:config]
67
+ product = tool_call.function.arguments.product
68
+
69
+ # Construct the URL for the endoflife API
70
+ url = config.tools.get_endoflife.url % { product: }
71
+
72
+ # Fetch the data from endoflife.date API
73
+ OllamaChat::Utils::Fetcher.get(
74
+ url,
75
+ headers: {
76
+ 'Accept' => 'application/json',
77
+ 'User-Agent' => OllamaChat::Chat.user_agent
78
+ },
79
+ debug: OllamaChat::EnvConfig::OLLAMA::CHAT::DEBUG
80
+ ) do |tmp|
81
+ # Parse the JSON response
82
+ data = JSON.parse(tmp.read, object_class: JSON::GenericObject)
83
+ return data
84
+ end
85
+ rescue StandardError => e
86
+ "Failed to fetch endoflife data for #{product}: #{e.class}: #{e.message}"
87
+ end
88
+
89
+ # Converts the tool to a hash representation.
90
+ #
91
+ # This method provides a standardized way to serialize the tool definition
92
+ # for use in tool calling systems.
93
+ #
94
+ # @return [Hash] a hash representation of the tool
95
+ def to_hash
96
+ tool.to_hash
97
+ end
98
+ end
@@ -0,0 +1,203 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'zip'
4
+
5
+ # A sensor implementation that fetches real-time temperature data from the
6
+ # German Weather Service (DWD). This sensor connects to DWD's open data API to
7
+ # retrieve the latest air temperature measurements for a specified weather
8
+ # station.
9
+ #
10
+ # The sensor expects DWD data files in ZIP format containing CSV data with the
11
+ # following structure:
12
+ # - Files are named like: 10minutenwerte_TU_00433_now.zip
13
+ # - Data is stored in CSV format with semicolon separators
14
+ # - Temperature values are in the TT_10 column (in Celsius)
15
+ # - Timestamps are in MESS_DATUM column (format: YYYYMMDDHHMM)
16
+ #
17
+ # Example usage:
18
+ # sensor = DWDSensor.new(sensor_id: 'station_1', station_id: '00433')
19
+ # temperature = sensor.measure
20
+ class DWDSensor
21
+ DEFAULT_URL_TEMPLATE = "https://opendata.dwd.de/climate_environment/CDC/observations_germany/climate/10_minutes/air_temperature/now/10minutenwerte_TU_%{station_id}_now.zip" # Template for the download URL
22
+
23
+ # Initializes a new sensor reader instance with the specified parameters.
24
+ #
25
+ # @param sensor_id [ String ] the unique identifier for the sensor
26
+ # @param station_id [ String ] the unique identifier for the weather station
27
+ # @param url_template [ String ] the URL template for fetching sensor data
28
+ # @param logger [ Logger ] the logger instance to use for logging messages
29
+ def initialize(sensor_id:, station_id:, url_template: DEFAULT_URL_TEMPLATE, logger: Logger.new($stderr))
30
+ @sensor_id = sensor_id
31
+ @station_id = station_id
32
+ @url_template = url_template
33
+ @last_modified = nil
34
+ @logger = logger
35
+ end
36
+
37
+ # The sensor_id method provides read-only access to the identifier of the
38
+ # sensor.
39
+ #
40
+ # @attr_reader [ String, Integer ] the unique identifier assigned to the sensor
41
+ # instance
42
+ attr_reader :sensor_id
43
+
44
+ # The measure method reads temperature data from a sensor and returns
45
+ # timestamped measurements.
46
+ #
47
+ # @return [ Array<Time, Float>, nil ] an array containing the timestamp and temperature reading,
48
+ # or nil if no valid temperature could be read
49
+ # @return [ nil ] if no temperature reading was available
50
+ def measure
51
+ @logger.info "Starting to read temperature from #{self.class} #{@sensor_id}…"
52
+ time, temp = read_temperature
53
+ if time && temp
54
+ @logger.info "Read temperature from #{self.class} #{@sensor_id} at #{time.iso8601}: #{temp}℃ "
55
+ [ time, temp ]
56
+ else
57
+ nil
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ # Reads the temperature data for the station, skipping the fetch if data is
64
+ # still current.
65
+ #
66
+ # This method first checks if the data fetching should be skipped based on
67
+ # the last modified timestamp comparison. If skipping is not appropriate, it
68
+ # proceeds to fetch the latest temperature data from DWD API.
69
+ #
70
+ # @return [nil] if the data fetching was skipped or failed
71
+ # @return [Array<Time, Float>] the result of the temperature fetching operation
72
+ # if successful
73
+ def read_temperature
74
+ if skip_fetching?
75
+ @logger.info "Data for station #{@station_id} still current from #{@last_modified.iso8601} => Skip fetching."
76
+ return
77
+ end
78
+ fetch_latest_temperature_from_dwd
79
+ rescue => e
80
+ @logger.error "Failed to fetch DWD data for station #{@station_id} => #{e.class}: #{e}"
81
+ nil
82
+ end
83
+
84
+ # Returns a URI object constructed from the URL template and station ID.
85
+ #
86
+ # @return [URI] a URI object created by interpolating the station ID into the URL template
87
+ def uri
88
+ URI(@url_template % { station_id: @station_id })
89
+ end
90
+
91
+ # Determines whether fetching should be skipped based on modification time
92
+ # comparisons.
93
+ #
94
+ # @return [ Boolean ] true if the resource has not been modified since last
95
+ # fetch, false otherwise
96
+ # @return [ Boolean ] false if no previous modification time is available
97
+ def skip_fetching?
98
+ @last_modified or return false
99
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
100
+ http.head(uri.path)
101
+ end
102
+ last_modified = Time.parse(response['Last-Modified'])
103
+ if last_modified > @last_modified
104
+ @last_modified = nil
105
+ false
106
+ else
107
+ true
108
+ end
109
+ end
110
+
111
+
112
+ # Fetches the latest temperature data from DWD API for the configured
113
+ # station.
114
+ #
115
+ # This method performs an HTTP GET request to the DWD open data API endpoint,
116
+ # retrieves the ZIP file containing weather data, and processes it to extract
117
+ # the most recent temperature reading. It handles various HTTP status codes
118
+ # and network errors gracefully.
119
+ #
120
+ # @return [Array<Time, Float>, nil] An array containing the timestamp and
121
+ # temperature value of the measurement, or nil if the fetch or processing
122
+ # fails
123
+ def fetch_latest_temperature_from_dwd
124
+ @logger.debug "Fetching DWD data from: #{uri}"
125
+ response = Net::HTTP.get_response(uri)
126
+ if response.code == '200'
127
+ @logger.info "Successfully fetched data for station #{@station_id}"
128
+
129
+ result = extract_from_zip(response.body)
130
+ @last_modified = Time.parse(response['Last-Modified'])
131
+ result
132
+ elsif response.code == '404'
133
+ @logger.error "File not found for station #{@station_id}: #{uri}"
134
+ nil
135
+ else
136
+ @logger.error "HTTP Error #{response.code}: #{response.message}"
137
+ nil
138
+ end
139
+ rescue => e
140
+ @logger.error "Network error fetching DWD data => #{e.class}: #{e}"
141
+ nil
142
+ end
143
+
144
+ # Extracts temperature data from a ZIP file containing DWD CSV data.
145
+ #
146
+ # This method takes raw ZIP file data, extracts the first entry (expected to
147
+ # be the CSV weather data), and processes it to return the latest temperature
148
+ # reading. The ZIP file is expected to contain CSV data with semicolon
149
+ # separators.
150
+ #
151
+ # @param zip_body [String] The raw binary content of the ZIP file
152
+ # @return [Array<Time, Float>, nil] An array containing the timestamp and temperature value,
153
+ # or nil if extraction or parsing fails
154
+ def extract_from_zip(zip_body)
155
+ # Create StringIO from response body
156
+ zip_data = StringIO.new(zip_body)
157
+
158
+ # Extract the first entry from ZIP file (should be the CSV data)
159
+ Zip::File.open_buffer(zip_data) do |zip_file|
160
+ entry = zip_file.first
161
+ if entry
162
+ content = entry.get_input_stream.read
163
+ @logger.debug "Extracted #{content.length} bytes from ZIP"
164
+ return parse_dwd_data(content)
165
+ else
166
+ @logger.error "No entries found in ZIP file for station #{@station_id}"
167
+ nil
168
+ end
169
+ end
170
+ rescue => e
171
+ @logger.error "Error extracting from ZIP: #{e}"
172
+ nil
173
+ end
174
+
175
+ # Parses DWD CSV data to extract the latest temperature reading for a weather
176
+ # station.
177
+ #
178
+ # @param data [String] The raw CSV data string from DWD containing weather measurements
179
+ # @return [Array<Time, Float>, nil] An array containing the timestamp and temperature value,
180
+ # or nil if no valid temperature data is found or if there are no data entries
181
+ def parse_dwd_data(data)
182
+ # Parse CSV data from DWD
183
+ csv_data = CSV.parse(data, headers: true, col_sep: ?;)
184
+
185
+ # Find the latest entry (most recent timestamp)
186
+ latest_entry, time = csv_data.
187
+ map { |row| [ row, Time.strptime(row['MESS_DATUM'], '%Y%m%d%H%M') ] }.
188
+ max_by(&:last)
189
+
190
+ if latest_entry
191
+ temp = latest_entry['TT_10']
192
+ if temp && temp != '9999' # 9999 indicates missing data
193
+ [ time, temp.to_f ]
194
+ else
195
+ @logger.warn "No valid temperature data found for station #{@station_id}"
196
+ nil
197
+ end
198
+ else
199
+ @logger.warn "No data entries found for station #{@station_id}"
200
+ nil
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,105 @@
1
+ require_relative 'weather/dwd_sensor'
2
+
3
+ # A module that provides tool registration and management for OllamaChat.
4
+ #
5
+ # The Tools module serves as a registry for available tools that can be
6
+ # invoked during chat conversations. It maintains a collection of
7
+ # registered tools and provides methods for registering new tools and
8
+ # accessing the complete set of available tools for use in chat
9
+ # interactions.
10
+ class OllamaChat::Tools::Weather
11
+ include Ollama
12
+
13
+ # The initialize method sets up the weather tool with its name.
14
+ def initialize
15
+ @name = 'get_current_weather'
16
+ end
17
+
18
+ # The name reader returns the name of the tool.
19
+ #
20
+ # @return [ String ] the name of the tool
21
+ attr_reader :name
22
+
23
+ # The tool method creates and returns a tool definition for getting
24
+ # current weather information.
25
+ #
26
+ # This method constructs a tool specification that can be used to invoke
27
+ # a weather information service. The tool definition includes the
28
+ # function name, description, and parameter specifications for location
29
+ # and temperature unit.
30
+ #
31
+ # @return [Ollama::Tool] a tool definition for retrieving current weather
32
+ # information
33
+ def tool
34
+ Tool.new(
35
+ type: 'function',
36
+ function: Tool::Function.new(
37
+ name:,
38
+ description: 'Get the current weather for a location',
39
+ parameters: Tool::Function::Parameters.new(
40
+ type: 'object',
41
+ properties: {
42
+ location: Tool::Function::Parameters::Property.new(
43
+ type: 'string',
44
+ description: 'The location to get the weather for, e.g. San Francisco, CA'
45
+ ),
46
+ temperature_unit: Tool::Function::Parameters::Property.new(
47
+ type: 'string',
48
+ description: "The unit to return the temperature in, either 'celsius' or 'fahrenheit'",
49
+ enum: %w[celsius fahrenheit]
50
+ )
51
+ },
52
+ required: %w[location temperature_unit]
53
+ )
54
+ )
55
+ )
56
+ end
57
+
58
+ # Executes a tool call to get current weather information.
59
+ #
60
+ # This method retrieves temperature data from a weather sensor using the
61
+ # DWD (German Weather Service) API and formats the result into a
62
+ # human-readable string including the temperature value, unit, and
63
+ # timestamp.
64
+ #
65
+ # @param tool_call [Object] the tool call object containing function
66
+ # details
67
+ # @param opts [Hash] additional options
68
+ # @option opts [ComplexConfig::Settings] :config the configuration object
69
+ #
70
+ # @return [String] a formatted weather report or error message
71
+ # @return [String] an error message if the weather data could not be
72
+ # retrieved
73
+ def execute(tool_call, **opts)
74
+ station_id = opts[:config].tools.get_current_weather.station_id
75
+ sensor = DWDSensor.new(
76
+ sensor_id: "dwd_#{station_id}",
77
+ station_id: ,
78
+ logger: Logger.new(STDOUT)
79
+ )
80
+
81
+ time, temp = sensor.measure
82
+
83
+ unless time && temp
84
+ return "Could not retrieve temperature for station #{station_id}"
85
+ end
86
+
87
+ unit = ?℃
88
+
89
+ if tool_call.function.arguments.temperature_unit == 'fahrenheit'
90
+ unit = ?℉
91
+ temp = temp * 9.0 / 5 + 32
92
+ end
93
+
94
+ "The temperature was %s %s at the time of %s" % [ temp, unit, time ]
95
+ rescue StandardError => e
96
+ "Failed to fetch weather for station #{station_id} #{e.class}: #{e.message}"
97
+ end
98
+
99
+ # The to_hash method converts the tool to a hash representation.
100
+ #
101
+ # @return [ Hash ] a hash representation of the tool
102
+ def to_hash
103
+ tool.to_hash
104
+ end
105
+ end
@@ -0,0 +1,32 @@
1
+ # A module that provides tool registration and management for OllamaChat.
2
+ #
3
+ # The Tools module serves as a registry for available tools that can be invoked
4
+ # during chat conversations. It maintains a collection of registered tools and
5
+ # provides methods for registering new tools and accessing the complete set of
6
+ # available tools for use in chat interactions.
7
+ module OllamaChat::Tools
8
+ class << self
9
+ # The registered attribute reader
10
+ #
11
+ # @return [ Hash ] the registered tools hash containing all available tools
12
+ attr_accessor :registered
13
+
14
+ # The register method adds a tool to the registry.
15
+ #
16
+ # @param tool [Object] the tool to be registered
17
+ #
18
+ # @return [Class] the class itself
19
+ def register(tool)
20
+ registered[tool.name] = tool
21
+ self
22
+ end
23
+ end
24
+
25
+ self.registered = {}
26
+ end
27
+ require 'ollama_chat/tools/weather'
28
+ OllamaChat::Tools.register OllamaChat::Tools::Weather.new
29
+ require 'ollama_chat/tools/cve'
30
+ OllamaChat::Tools.register OllamaChat::Tools::CVE.new
31
+ require 'ollama_chat/tools/endoflife'
32
+ OllamaChat::Tools.register OllamaChat::Tools::EndOfLife.new
@@ -1,6 +1,6 @@
1
1
  module OllamaChat
2
2
  # OllamaChat version
3
- VERSION = '0.0.58'
3
+ VERSION = '0.0.59'
4
4
  VERSION_ARRAY = VERSION.split('.').map(&:to_i) # :nodoc:
5
5
  VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
6
6
  VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
data/lib/ollama_chat.rb CHANGED
@@ -40,4 +40,6 @@ require 'ollama_chat/conversation'
40
40
  require 'ollama_chat/input_content'
41
41
  require 'ollama_chat/message_editing'
42
42
  require 'ollama_chat/env_config'
43
+ require 'ollama_chat/tools'
44
+ require 'ollama_chat/tool_calling'
43
45
  require 'ollama_chat/chat'
data/ollama_chat.gemspec CHANGED
@@ -1,9 +1,9 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: ollama_chat 0.0.58 ruby lib
2
+ # stub: ollama_chat 0.0.59 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "ollama_chat".freeze
6
- s.version = "0.0.58".freeze
6
+ s.version = "0.0.59".freeze
7
7
 
8
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
9
9
  s.require_paths = ["lib".freeze]
@@ -12,15 +12,15 @@ Gem::Specification.new do |s|
12
12
  s.description = "The app provides a command-line interface (CLI) to an Ollama AI model,\nallowing users to engage in text-based conversations and generate\nhuman-like responses. Users can import data from local files or web pages,\nwhich are then processed through three different modes: fully importing the\ncontent into the conversation context, summarizing the information for\nconcise reference, or storing it in an embedding vector database for later\nretrieval based on the conversation.\n".freeze
13
13
  s.email = "flori@ping.de".freeze
14
14
  s.executables = ["ollama_chat".freeze, "ollama_chat_send".freeze]
15
- s.extra_rdoc_files = ["README.md".freeze, "lib/ollama_chat.rb".freeze, "lib/ollama_chat/chat.rb".freeze, "lib/ollama_chat/clipboard.rb".freeze, "lib/ollama_chat/conversation.rb".freeze, "lib/ollama_chat/dialog.rb".freeze, "lib/ollama_chat/document_cache.rb".freeze, "lib/ollama_chat/env_config.rb".freeze, "lib/ollama_chat/follow_chat.rb".freeze, "lib/ollama_chat/history.rb".freeze, "lib/ollama_chat/information.rb".freeze, "lib/ollama_chat/input_content.rb".freeze, "lib/ollama_chat/kramdown_ansi.rb".freeze, "lib/ollama_chat/message_editing.rb".freeze, "lib/ollama_chat/message_format.rb".freeze, "lib/ollama_chat/message_list.rb".freeze, "lib/ollama_chat/message_output.rb".freeze, "lib/ollama_chat/model_handling.rb".freeze, "lib/ollama_chat/ollama_chat_config.rb".freeze, "lib/ollama_chat/parsing.rb".freeze, "lib/ollama_chat/redis_cache.rb".freeze, "lib/ollama_chat/server_socket.rb".freeze, "lib/ollama_chat/source_fetching.rb".freeze, "lib/ollama_chat/state_selectors.rb".freeze, "lib/ollama_chat/switches.rb".freeze, "lib/ollama_chat/think_control.rb".freeze, "lib/ollama_chat/utils.rb".freeze, "lib/ollama_chat/utils/cache_fetcher.rb".freeze, "lib/ollama_chat/utils/chooser.rb".freeze, "lib/ollama_chat/utils/fetcher.rb".freeze, "lib/ollama_chat/utils/file_argument.rb".freeze, "lib/ollama_chat/version.rb".freeze, "lib/ollama_chat/vim.rb".freeze, "lib/ollama_chat/web_searching.rb".freeze]
16
- s.files = [".utilsrc".freeze, "CHANGES.md".freeze, "Gemfile".freeze, "README.md".freeze, "Rakefile".freeze, "bin/ollama_chat".freeze, "bin/ollama_chat_send".freeze, "config/searxng/settings.yml".freeze, "docker-compose.yml".freeze, "lib/ollama_chat.rb".freeze, "lib/ollama_chat/chat.rb".freeze, "lib/ollama_chat/clipboard.rb".freeze, "lib/ollama_chat/conversation.rb".freeze, "lib/ollama_chat/dialog.rb".freeze, "lib/ollama_chat/document_cache.rb".freeze, "lib/ollama_chat/env_config.rb".freeze, "lib/ollama_chat/follow_chat.rb".freeze, "lib/ollama_chat/history.rb".freeze, "lib/ollama_chat/information.rb".freeze, "lib/ollama_chat/input_content.rb".freeze, "lib/ollama_chat/kramdown_ansi.rb".freeze, "lib/ollama_chat/message_editing.rb".freeze, "lib/ollama_chat/message_format.rb".freeze, "lib/ollama_chat/message_list.rb".freeze, "lib/ollama_chat/message_output.rb".freeze, "lib/ollama_chat/model_handling.rb".freeze, "lib/ollama_chat/ollama_chat_config.rb".freeze, "lib/ollama_chat/ollama_chat_config/default_config.yml".freeze, "lib/ollama_chat/parsing.rb".freeze, "lib/ollama_chat/redis_cache.rb".freeze, "lib/ollama_chat/server_socket.rb".freeze, "lib/ollama_chat/source_fetching.rb".freeze, "lib/ollama_chat/state_selectors.rb".freeze, "lib/ollama_chat/switches.rb".freeze, "lib/ollama_chat/think_control.rb".freeze, "lib/ollama_chat/utils.rb".freeze, "lib/ollama_chat/utils/cache_fetcher.rb".freeze, "lib/ollama_chat/utils/chooser.rb".freeze, "lib/ollama_chat/utils/fetcher.rb".freeze, "lib/ollama_chat/utils/file_argument.rb".freeze, "lib/ollama_chat/version.rb".freeze, "lib/ollama_chat/vim.rb".freeze, "lib/ollama_chat/web_searching.rb".freeze, "ollama_chat.gemspec".freeze, "redis/redis.conf".freeze, "spec/assets/api_show.json".freeze, "spec/assets/api_tags.json".freeze, "spec/assets/api_version.json".freeze, "spec/assets/conversation.json".freeze, "spec/assets/duckduckgo.html".freeze, "spec/assets/example.atom".freeze, "spec/assets/example.csv".freeze, "spec/assets/example.html".freeze, "spec/assets/example.pdf".freeze, "spec/assets/example.ps".freeze, "spec/assets/example.rb".freeze, "spec/assets/example.rss".freeze, "spec/assets/example.xml".freeze, "spec/assets/example_with_quote.html".freeze, "spec/assets/kitten.jpg".freeze, "spec/assets/prompt.txt".freeze, "spec/assets/searxng.json".freeze, "spec/ollama_chat/chat_spec.rb".freeze, "spec/ollama_chat/clipboard_spec.rb".freeze, "spec/ollama_chat/follow_chat_spec.rb".freeze, "spec/ollama_chat/information_spec.rb".freeze, "spec/ollama_chat/input_content_spec.rb".freeze, "spec/ollama_chat/kramdown_ansi_spec.rb".freeze, "spec/ollama_chat/message_editing_spec.rb".freeze, "spec/ollama_chat/message_list_spec.rb".freeze, "spec/ollama_chat/message_output_spec.rb".freeze, "spec/ollama_chat/model_handling_spec.rb".freeze, "spec/ollama_chat/parsing_spec.rb".freeze, "spec/ollama_chat/redis_cache_spec.rb".freeze, "spec/ollama_chat/server_socket_spec.rb".freeze, "spec/ollama_chat/source_fetching_spec.rb".freeze, "spec/ollama_chat/state_selectors_spec.rb".freeze, "spec/ollama_chat/switches_spec.rb".freeze, "spec/ollama_chat/think_control_spec.rb".freeze, "spec/ollama_chat/utils/cache_fetcher_spec.rb".freeze, "spec/ollama_chat/utils/fetcher_spec.rb".freeze, "spec/ollama_chat/utils/file_argument_spec.rb".freeze, "spec/ollama_chat/vim_spec.rb".freeze, "spec/ollama_chat/web_searching_spec.rb".freeze, "spec/spec_helper.rb".freeze, "tmp/.keep".freeze]
15
+ s.extra_rdoc_files = ["README.md".freeze, "lib/ollama_chat.rb".freeze, "lib/ollama_chat/chat.rb".freeze, "lib/ollama_chat/clipboard.rb".freeze, "lib/ollama_chat/conversation.rb".freeze, "lib/ollama_chat/dialog.rb".freeze, "lib/ollama_chat/document_cache.rb".freeze, "lib/ollama_chat/env_config.rb".freeze, "lib/ollama_chat/follow_chat.rb".freeze, "lib/ollama_chat/history.rb".freeze, "lib/ollama_chat/information.rb".freeze, "lib/ollama_chat/input_content.rb".freeze, "lib/ollama_chat/kramdown_ansi.rb".freeze, "lib/ollama_chat/message_editing.rb".freeze, "lib/ollama_chat/message_format.rb".freeze, "lib/ollama_chat/message_list.rb".freeze, "lib/ollama_chat/message_output.rb".freeze, "lib/ollama_chat/model_handling.rb".freeze, "lib/ollama_chat/ollama_chat_config.rb".freeze, "lib/ollama_chat/parsing.rb".freeze, "lib/ollama_chat/redis_cache.rb".freeze, "lib/ollama_chat/server_socket.rb".freeze, "lib/ollama_chat/source_fetching.rb".freeze, "lib/ollama_chat/state_selectors.rb".freeze, "lib/ollama_chat/switches.rb".freeze, "lib/ollama_chat/think_control.rb".freeze, "lib/ollama_chat/tool_calling.rb".freeze, "lib/ollama_chat/tools.rb".freeze, "lib/ollama_chat/tools/cve.rb".freeze, "lib/ollama_chat/tools/endoflife.rb".freeze, "lib/ollama_chat/tools/weather.rb".freeze, "lib/ollama_chat/tools/weather/dwd_sensor.rb".freeze, "lib/ollama_chat/utils.rb".freeze, "lib/ollama_chat/utils/cache_fetcher.rb".freeze, "lib/ollama_chat/utils/chooser.rb".freeze, "lib/ollama_chat/utils/fetcher.rb".freeze, "lib/ollama_chat/utils/file_argument.rb".freeze, "lib/ollama_chat/version.rb".freeze, "lib/ollama_chat/vim.rb".freeze, "lib/ollama_chat/web_searching.rb".freeze]
16
+ s.files = [".utilsrc".freeze, "CHANGES.md".freeze, "Gemfile".freeze, "README.md".freeze, "Rakefile".freeze, "bin/ollama_chat".freeze, "bin/ollama_chat_send".freeze, "config/searxng/settings.yml".freeze, "docker-compose.yml".freeze, "lib/ollama_chat.rb".freeze, "lib/ollama_chat/chat.rb".freeze, "lib/ollama_chat/clipboard.rb".freeze, "lib/ollama_chat/conversation.rb".freeze, "lib/ollama_chat/dialog.rb".freeze, "lib/ollama_chat/document_cache.rb".freeze, "lib/ollama_chat/env_config.rb".freeze, "lib/ollama_chat/follow_chat.rb".freeze, "lib/ollama_chat/history.rb".freeze, "lib/ollama_chat/information.rb".freeze, "lib/ollama_chat/input_content.rb".freeze, "lib/ollama_chat/kramdown_ansi.rb".freeze, "lib/ollama_chat/message_editing.rb".freeze, "lib/ollama_chat/message_format.rb".freeze, "lib/ollama_chat/message_list.rb".freeze, "lib/ollama_chat/message_output.rb".freeze, "lib/ollama_chat/model_handling.rb".freeze, "lib/ollama_chat/ollama_chat_config.rb".freeze, "lib/ollama_chat/ollama_chat_config/default_config.yml".freeze, "lib/ollama_chat/parsing.rb".freeze, "lib/ollama_chat/redis_cache.rb".freeze, "lib/ollama_chat/server_socket.rb".freeze, "lib/ollama_chat/source_fetching.rb".freeze, "lib/ollama_chat/state_selectors.rb".freeze, "lib/ollama_chat/switches.rb".freeze, "lib/ollama_chat/think_control.rb".freeze, "lib/ollama_chat/tool_calling.rb".freeze, "lib/ollama_chat/tools.rb".freeze, "lib/ollama_chat/tools/cve.rb".freeze, "lib/ollama_chat/tools/endoflife.rb".freeze, "lib/ollama_chat/tools/weather.rb".freeze, "lib/ollama_chat/tools/weather/dwd_sensor.rb".freeze, "lib/ollama_chat/utils.rb".freeze, "lib/ollama_chat/utils/cache_fetcher.rb".freeze, "lib/ollama_chat/utils/chooser.rb".freeze, "lib/ollama_chat/utils/fetcher.rb".freeze, "lib/ollama_chat/utils/file_argument.rb".freeze, "lib/ollama_chat/version.rb".freeze, "lib/ollama_chat/vim.rb".freeze, "lib/ollama_chat/web_searching.rb".freeze, "ollama_chat.gemspec".freeze, "redis/redis.conf".freeze, "spec/assets/api_show.json".freeze, "spec/assets/api_tags.json".freeze, "spec/assets/api_version.json".freeze, "spec/assets/conversation.json".freeze, "spec/assets/duckduckgo.html".freeze, "spec/assets/example.atom".freeze, "spec/assets/example.csv".freeze, "spec/assets/example.html".freeze, "spec/assets/example.pdf".freeze, "spec/assets/example.ps".freeze, "spec/assets/example.rb".freeze, "spec/assets/example.rss".freeze, "spec/assets/example.xml".freeze, "spec/assets/example_with_quote.html".freeze, "spec/assets/kitten.jpg".freeze, "spec/assets/prompt.txt".freeze, "spec/assets/searxng.json".freeze, "spec/ollama_chat/chat_spec.rb".freeze, "spec/ollama_chat/clipboard_spec.rb".freeze, "spec/ollama_chat/follow_chat_spec.rb".freeze, "spec/ollama_chat/information_spec.rb".freeze, "spec/ollama_chat/input_content_spec.rb".freeze, "spec/ollama_chat/kramdown_ansi_spec.rb".freeze, "spec/ollama_chat/message_editing_spec.rb".freeze, "spec/ollama_chat/message_list_spec.rb".freeze, "spec/ollama_chat/message_output_spec.rb".freeze, "spec/ollama_chat/model_handling_spec.rb".freeze, "spec/ollama_chat/parsing_spec.rb".freeze, "spec/ollama_chat/redis_cache_spec.rb".freeze, "spec/ollama_chat/server_socket_spec.rb".freeze, "spec/ollama_chat/source_fetching_spec.rb".freeze, "spec/ollama_chat/state_selectors_spec.rb".freeze, "spec/ollama_chat/switches_spec.rb".freeze, "spec/ollama_chat/think_control_spec.rb".freeze, "spec/ollama_chat/tools/cve_spec.rb".freeze, "spec/ollama_chat/tools/endoflife_spec.rb".freeze, "spec/ollama_chat/tools/weather_spec.rb".freeze, "spec/ollama_chat/utils/cache_fetcher_spec.rb".freeze, "spec/ollama_chat/utils/fetcher_spec.rb".freeze, "spec/ollama_chat/utils/file_argument_spec.rb".freeze, "spec/ollama_chat/vim_spec.rb".freeze, "spec/ollama_chat/web_searching_spec.rb".freeze, "spec/spec_helper.rb".freeze, "tmp/.keep".freeze]
17
17
  s.homepage = "https://github.com/flori/ollama_chat".freeze
18
18
  s.licenses = ["MIT".freeze]
19
19
  s.rdoc_options = ["--title".freeze, "OllamaChat - A command-line interface (CLI) for interacting with an Ollama AI model.".freeze, "--main".freeze, "README.md".freeze]
20
20
  s.required_ruby_version = Gem::Requirement.new(">= 3.2".freeze)
21
21
  s.rubygems_version = "4.0.3".freeze
22
22
  s.summary = "A command-line interface (CLI) for interacting with an Ollama AI model.".freeze
23
- s.test_files = ["spec/assets/example.rb".freeze, "spec/ollama_chat/chat_spec.rb".freeze, "spec/ollama_chat/clipboard_spec.rb".freeze, "spec/ollama_chat/follow_chat_spec.rb".freeze, "spec/ollama_chat/information_spec.rb".freeze, "spec/ollama_chat/input_content_spec.rb".freeze, "spec/ollama_chat/kramdown_ansi_spec.rb".freeze, "spec/ollama_chat/message_editing_spec.rb".freeze, "spec/ollama_chat/message_list_spec.rb".freeze, "spec/ollama_chat/message_output_spec.rb".freeze, "spec/ollama_chat/model_handling_spec.rb".freeze, "spec/ollama_chat/parsing_spec.rb".freeze, "spec/ollama_chat/redis_cache_spec.rb".freeze, "spec/ollama_chat/server_socket_spec.rb".freeze, "spec/ollama_chat/source_fetching_spec.rb".freeze, "spec/ollama_chat/state_selectors_spec.rb".freeze, "spec/ollama_chat/switches_spec.rb".freeze, "spec/ollama_chat/think_control_spec.rb".freeze, "spec/ollama_chat/utils/cache_fetcher_spec.rb".freeze, "spec/ollama_chat/utils/fetcher_spec.rb".freeze, "spec/ollama_chat/utils/file_argument_spec.rb".freeze, "spec/ollama_chat/vim_spec.rb".freeze, "spec/ollama_chat/web_searching_spec.rb".freeze, "spec/spec_helper.rb".freeze]
23
+ s.test_files = ["spec/assets/example.rb".freeze, "spec/ollama_chat/chat_spec.rb".freeze, "spec/ollama_chat/clipboard_spec.rb".freeze, "spec/ollama_chat/follow_chat_spec.rb".freeze, "spec/ollama_chat/information_spec.rb".freeze, "spec/ollama_chat/input_content_spec.rb".freeze, "spec/ollama_chat/kramdown_ansi_spec.rb".freeze, "spec/ollama_chat/message_editing_spec.rb".freeze, "spec/ollama_chat/message_list_spec.rb".freeze, "spec/ollama_chat/message_output_spec.rb".freeze, "spec/ollama_chat/model_handling_spec.rb".freeze, "spec/ollama_chat/parsing_spec.rb".freeze, "spec/ollama_chat/redis_cache_spec.rb".freeze, "spec/ollama_chat/server_socket_spec.rb".freeze, "spec/ollama_chat/source_fetching_spec.rb".freeze, "spec/ollama_chat/state_selectors_spec.rb".freeze, "spec/ollama_chat/switches_spec.rb".freeze, "spec/ollama_chat/think_control_spec.rb".freeze, "spec/ollama_chat/tools/cve_spec.rb".freeze, "spec/ollama_chat/tools/endoflife_spec.rb".freeze, "spec/ollama_chat/tools/weather_spec.rb".freeze, "spec/ollama_chat/utils/cache_fetcher_spec.rb".freeze, "spec/ollama_chat/utils/fetcher_spec.rb".freeze, "spec/ollama_chat/utils/file_argument_spec.rb".freeze, "spec/ollama_chat/vim_spec.rb".freeze, "spec/ollama_chat/web_searching_spec.rb".freeze, "spec/spec_helper.rb".freeze]
24
24
 
25
25
  s.specification_version = 4
26
26
 
@@ -52,4 +52,5 @@ Gem::Specification.new do |s|
52
52
  s.add_runtime_dependency(%q<csv>.freeze, ["~> 3.0".freeze])
53
53
  s.add_runtime_dependency(%q<const_conf>.freeze, ["~> 0.3".freeze])
54
54
  s.add_runtime_dependency(%q<context_spook>.freeze, ["~> 1.5".freeze])
55
+ s.add_runtime_dependency(%q<rubyzip>.freeze, ["~> 3.0".freeze])
55
56
  end
@@ -177,6 +177,23 @@ describe OllamaChat::Chat, protect_env: true do
177
177
  expect(chat.handle_input("/load ./some_file")).to eq :next
178
178
  end
179
179
 
180
+ describe 'tools' do
181
+ it 'returns :next when input is "/tools"' do
182
+ expect(chat).to receive(:list_tools)
183
+ expect(chat.handle_input("/tools")).to eq :next
184
+ end
185
+
186
+ it 'returns :next when input is "/tools enable"' do
187
+ expect(chat).to receive(:enable_tool)
188
+ expect(chat.handle_input("/tools enable")).to eq :next
189
+ end
190
+
191
+ it 'returns :next when input is "/tools disable"' do
192
+ expect(chat).to receive(:disable_tool)
193
+ expect(chat.handle_input("/tools disable")).to eq :next
194
+ end
195
+ end
196
+
180
197
  it 'returns :next when input is "/config"' do
181
198
  expect(chat).to receive(:display_config)
182
199
  expect(chat.handle_input("/config")).to eq :next
@@ -0,0 +1,71 @@
1
+ require 'spec_helper'
2
+
3
+ describe OllamaChat::Tools::CVE do
4
+ let :chat do
5
+ OllamaChat::Chat.new argv: chat_default_config
6
+ end
7
+
8
+ connect_to_ollama_server
9
+
10
+ it 'can have name' do
11
+ expect(described_class.new.name).to eq 'get_cve'
12
+ end
13
+
14
+ it 'can have tool' do
15
+ expect(described_class.new.tool).to be_a Ollama::Tool
16
+ end
17
+
18
+ it 'can be executed successfully' do
19
+ # Mock the fetcher to return a valid CVE response
20
+ cve_id = 'CVE-2023-12345'
21
+ tool_call = double(
22
+ 'ToolCall',
23
+ function: double(
24
+ name: 'get_cve',
25
+ arguments: double(
26
+ cve_id: cve_id
27
+ )
28
+ )
29
+ )
30
+
31
+ url = chat.config.tools.get_cve.url
32
+
33
+ # Stub the HTTP request
34
+ stub_request(:get, url % { cve_id: cve_id })
35
+ .to_return(
36
+ status: 200,
37
+ body: '{"id": "CVE-2023-12345", "description": "Test vulnerability description"}',
38
+ headers: { 'Content-Type' => 'application/json' }
39
+ )
40
+
41
+ result = described_class.new.execute(tool_call, config: chat.config)
42
+ expect(result.id).to eq 'CVE-2023-12345'
43
+ expect(result.description).to include('Test vulnerability description')
44
+ end
45
+
46
+ it 'can handle execution errors gracefully' do
47
+ cve_id = 'CVE-2023-99999'
48
+ tool_call = double(
49
+ 'ToolCall',
50
+ function: double(
51
+ name: 'get_cve',
52
+ arguments: double(
53
+ cve_id: cve_id
54
+ )
55
+ )
56
+ )
57
+
58
+ url = chat.config.tools.get_cve.url
59
+
60
+ stub_request(:get, url % { cve_id: })
61
+ .to_return(status: 404, body: 'Not Found')
62
+
63
+ result = described_class.new.execute(tool_call, config: chat.config)
64
+ expect(result).to include('Failed to fetch CVE')
65
+ expect(result).to include(cve_id)
66
+ end
67
+
68
+ it 'can be converted to hash' do
69
+ expect(described_class.new.to_hash).to be_a Hash
70
+ end
71
+ end
@@ -0,0 +1,72 @@
1
+ require 'spec_helper'
2
+
3
+ describe OllamaChat::Tools::EndOfLife do
4
+ let :chat do
5
+ OllamaChat::Chat.new argv: chat_default_config
6
+ end
7
+
8
+ connect_to_ollama_server
9
+
10
+ it 'can have name' do
11
+ expect(described_class.new.name).to eq 'get_endoflife'
12
+ end
13
+
14
+ it 'can have tool' do
15
+ expect(described_class.new.tool).to be_a Ollama::Tool
16
+ end
17
+
18
+ it 'can be executed successfully' do
19
+ # Mock the fetcher to return a valid endoflife response
20
+ product = 'ruby'
21
+ tool_call = double(
22
+ 'ToolCall',
23
+ function: double(
24
+ name: 'get_endoflife',
25
+ arguments: double(
26
+ product: product
27
+ )
28
+ )
29
+ )
30
+
31
+ url = chat.config.tools.get_endoflife.url
32
+
33
+ # Stub the HTTP request
34
+ stub_request(:get, url % { product: })
35
+ .to_return(
36
+ status: 200,
37
+ body: '{ "cycle": "3.1", "releaseDate": "2023-05-01", "eol": "2026-05-01" }',
38
+ headers: { 'Content-Type' => 'application/json' }
39
+ )
40
+
41
+ result = described_class.new.execute(tool_call, config: chat.config)
42
+ expect(result.cycle).to eq '3.1'
43
+ expect(result.releaseDate).to eq '2023-05-01'
44
+ expect(result.eol).to eq '2026-05-01'
45
+ end
46
+
47
+ it 'can handle execution errors gracefully' do
48
+ product = 'nonexistent-product'
49
+ tool_call = double(
50
+ 'ToolCall',
51
+ function: double(
52
+ name: 'get_endoflife',
53
+ arguments: double(
54
+ product: product
55
+ )
56
+ )
57
+ )
58
+
59
+ url = chat.config.tools.get_endoflife.url
60
+
61
+ stub_request(:get, url % { product: product })
62
+ .to_return(status: 404, body: 'Not Found')
63
+
64
+ result = described_class.new.execute(tool_call, config: chat.config)
65
+ expect(result).to include('Failed to fetch endoflife data')
66
+ expect(result).to include(product)
67
+ end
68
+
69
+ it 'can be converted to hash' do
70
+ expect(described_class.new.to_hash).to be_a Hash
71
+ end
72
+ end
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+
3
+ describe OllamaChat::Tools::Weather do
4
+ let :chat do
5
+ OllamaChat::Chat.new argv: chat_default_config
6
+ end
7
+
8
+ connect_to_ollama_server
9
+
10
+ it 'can have name' do
11
+ expect(described_class.new.name).to eq 'get_current_weather'
12
+ end
13
+
14
+ it 'can have tool' do
15
+ expect(described_class.new.tool).to be_a Ollama::Tool
16
+ end
17
+
18
+ it 'can be executed for celsius' do
19
+ expect(DWDSensor).to receive(:new).and_return(
20
+ double(measure: [ Time.now, 23.0 ])
21
+ )
22
+ tool_call = double(
23
+ 'ToolCall',
24
+ function: double(
25
+ name: 'get_current_weather',
26
+ arguments: double(
27
+ location: 'Berlin',
28
+ temperature_unit: 'celsius'
29
+ )
30
+ )
31
+ )
32
+ expect(
33
+ described_class.new.execute(tool_call, config: chat.config)
34
+ ).to match(/The temperature was 23.0 ℃ at the time of /)
35
+ end
36
+
37
+ it 'can be executed for fahrenheit' do
38
+ expect(DWDSensor).to receive(:new).and_return(
39
+ double(measure: [ Time.now, 23.0 ])
40
+ )
41
+ tool_call = double(
42
+ 'ToolCall',
43
+ function: double(
44
+ name: 'get_current_weather',
45
+ arguments: double(
46
+ location: 'Berlin',
47
+ temperature_unit: 'fahrenheit'
48
+ )
49
+ )
50
+ )
51
+ expect(
52
+ described_class.new.execute(tool_call, config: chat.config)
53
+ ).to match(/The temperature was 73.4 ℉ at the time of /)
54
+ end
55
+
56
+ it 'can be converted to hash' do
57
+ expect(described_class.new.to_hash).to be_a Hash
58
+ end
59
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ollama_chat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.58
4
+ version: 0.0.59
5
5
  platform: ruby
6
6
  authors:
7
7
  - Florian Frank
@@ -407,6 +407,20 @@ dependencies:
407
407
  - - "~>"
408
408
  - !ruby/object:Gem::Version
409
409
  version: '1.5'
410
+ - !ruby/object:Gem::Dependency
411
+ name: rubyzip
412
+ requirement: !ruby/object:Gem::Requirement
413
+ requirements:
414
+ - - "~>"
415
+ - !ruby/object:Gem::Version
416
+ version: '3.0'
417
+ type: :runtime
418
+ prerelease: false
419
+ version_requirements: !ruby/object:Gem::Requirement
420
+ requirements:
421
+ - - "~>"
422
+ - !ruby/object:Gem::Version
423
+ version: '3.0'
410
424
  description: |
411
425
  The app provides a command-line interface (CLI) to an Ollama AI model,
412
426
  allowing users to engage in text-based conversations and generate
@@ -447,6 +461,12 @@ extra_rdoc_files:
447
461
  - lib/ollama_chat/state_selectors.rb
448
462
  - lib/ollama_chat/switches.rb
449
463
  - lib/ollama_chat/think_control.rb
464
+ - lib/ollama_chat/tool_calling.rb
465
+ - lib/ollama_chat/tools.rb
466
+ - lib/ollama_chat/tools/cve.rb
467
+ - lib/ollama_chat/tools/endoflife.rb
468
+ - lib/ollama_chat/tools/weather.rb
469
+ - lib/ollama_chat/tools/weather/dwd_sensor.rb
450
470
  - lib/ollama_chat/utils.rb
451
471
  - lib/ollama_chat/utils/cache_fetcher.rb
452
472
  - lib/ollama_chat/utils/chooser.rb
@@ -491,6 +511,12 @@ files:
491
511
  - lib/ollama_chat/state_selectors.rb
492
512
  - lib/ollama_chat/switches.rb
493
513
  - lib/ollama_chat/think_control.rb
514
+ - lib/ollama_chat/tool_calling.rb
515
+ - lib/ollama_chat/tools.rb
516
+ - lib/ollama_chat/tools/cve.rb
517
+ - lib/ollama_chat/tools/endoflife.rb
518
+ - lib/ollama_chat/tools/weather.rb
519
+ - lib/ollama_chat/tools/weather/dwd_sensor.rb
494
520
  - lib/ollama_chat/utils.rb
495
521
  - lib/ollama_chat/utils/cache_fetcher.rb
496
522
  - lib/ollama_chat/utils/chooser.rb
@@ -535,6 +561,9 @@ files:
535
561
  - spec/ollama_chat/state_selectors_spec.rb
536
562
  - spec/ollama_chat/switches_spec.rb
537
563
  - spec/ollama_chat/think_control_spec.rb
564
+ - spec/ollama_chat/tools/cve_spec.rb
565
+ - spec/ollama_chat/tools/endoflife_spec.rb
566
+ - spec/ollama_chat/tools/weather_spec.rb
538
567
  - spec/ollama_chat/utils/cache_fetcher_spec.rb
539
568
  - spec/ollama_chat/utils/fetcher_spec.rb
540
569
  - spec/ollama_chat/utils/file_argument_spec.rb
@@ -586,6 +615,9 @@ test_files:
586
615
  - spec/ollama_chat/state_selectors_spec.rb
587
616
  - spec/ollama_chat/switches_spec.rb
588
617
  - spec/ollama_chat/think_control_spec.rb
618
+ - spec/ollama_chat/tools/cve_spec.rb
619
+ - spec/ollama_chat/tools/endoflife_spec.rb
620
+ - spec/ollama_chat/tools/weather_spec.rb
589
621
  - spec/ollama_chat/utils/cache_fetcher_spec.rb
590
622
  - spec/ollama_chat/utils/fetcher_spec.rb
591
623
  - spec/ollama_chat/utils/file_argument_spec.rb