pedicab 0.3.1 → 0.3.3

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/API.md +401 -0
  3. data/EXAMPLES.md +884 -0
  4. data/Gemfile.lock +10 -24
  5. data/INSTALLATION.md +652 -0
  6. data/README.md +329 -10
  7. data/lib/pedicab/#city.rb# +27 -0
  8. data/lib/pedicab/ride.rb +60 -81
  9. data/lib/pedicab/version.rb +1 -1
  10. data/lib/pedicab.py +3 -8
  11. data/lib/pedicab.rb +141 -133
  12. metadata +6 -89
  13. data/#README.md# +0 -51
  14. data/books/Arnold_Bennett-How_to_Live_on_24_Hours_a_Day.txt +0 -1247
  15. data/books/Edward_L_Bernays-crystallizing_public_opinion.txt +0 -4422
  16. data/books/Emma_Goldman-Anarchism_and_Other_Essays.txt +0 -7654
  17. data/books/Office_of_Strategic_Services-Simple_Sabotage_Field_Manual.txt +0 -1057
  18. data/books/Sigmund_Freud-Group_Psychology_and_The_Analysis_of_The_Ego.txt +0 -2360
  19. data/books/Steve_Hassan-The_Bite_Model.txt +0 -130
  20. data/books/Steve_Hassan-The_Bite_Model.txt~ +0 -132
  21. data/books/Sun_Tzu-Art_of_War.txt +0 -159
  22. data/books/Sun_Tzu-Art_of_War.txt~ +0 -166
  23. data/books/US-Constitution.txt +0 -502
  24. data/books/US-Constitution.txt~ +0 -502
  25. data/books/cia-kubark.txt +0 -4637
  26. data/books/machiavelli-the_prince.txt +0 -4599
  27. data/books/sun_tzu-art_of_war.txt +0 -1017
  28. data/books/us_army-bayonette.txt +0 -843
  29. data/lib/pedicab/calc.rb~ +0 -8
  30. data/lib/pedicab/link.rb +0 -38
  31. data/lib/pedicab/link.rb~ +0 -14
  32. data/lib/pedicab/mark.rb +0 -9
  33. data/lib/pedicab/mark.rb~ +0 -5
  34. data/lib/pedicab/on.rb +0 -6
  35. data/lib/pedicab/on.rb~ +0 -6
  36. data/lib/pedicab/poke.rb +0 -14
  37. data/lib/pedicab/poke.rb~ +0 -15
  38. data/lib/pedicab/query.rb +0 -92
  39. data/lib/pedicab/query.rb~ +0 -93
  40. data/lib/pedicab/rank.rb +0 -92
  41. data/lib/pedicab/rank.rb~ +0 -89
  42. data/lib/pedicab/ride.rb~ +0 -101
  43. data/lib/pedicab.sh~ +0 -3
data/README.md CHANGED
@@ -1,23 +1,342 @@
1
1
  # Pedicab
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/pedicab`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ A lightweight Ruby framework for conversational AI interfaces using local LLM models. Pedicab provides a clean, intuitive API for building AI-powered applications with context management and structured responses.
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ ## Overview
6
+
7
+ Pedicab is designed to be "the fastest LLMs in the west" - a streamlined interface for local language model inference. It focuses on the essential mechanics of conversational AI while maintaining simplicity and performance.
8
+
9
+ ### Core Features
10
+
11
+ - **Local LLM Integration**: Uses GGUF model files with Python llama-cpp backend
12
+ - **Context Management**: Automatic conversation context tracking and management
13
+ - **Elegant Syntax**: Intuitive `[]` and `<<` operators for different interaction patterns
14
+ - **State Management**: Persistent conversation state with reset capabilities
15
+ - **Performance Tracking**: Built-in benchmarking and timing information
16
+ - **Conditional Logic**: Support for boolean conditions and flow control
6
17
 
7
18
  ## Installation
8
19
 
9
- Install the gem and add to the application's Gemfile by executing:
20
+ ### Prerequisites
21
+
22
+ 1. **Ruby** >= 2.6.0
23
+ 2. **Python** with llama-cpp-python package
24
+ 3. **Local LLM Models** in GGUF format
25
+
26
+ ### Setup
27
+
28
+ 1. Install the gem:
29
+ ```bash
30
+ gem install pedicab
31
+ ```
32
+
33
+ 2. Install Python dependencies:
34
+ ```bash
35
+ pip install llama-cpp-python
36
+ ```
37
+
38
+ 3. Download LLM models to `/models/` directory:
39
+ ```bash
40
+ # Example: Download a model
41
+ wget https://example.com/model.gguf -O /models/qwen.gguf
42
+ ```
43
+
44
+ 4. Set up the Python backend (included with the gem)
45
+
46
+ ## Quick Start
47
+
48
+ ### Sample Pry Console
49
+
50
+ ```ruby
51
+ require 'pry'
52
+ require "bundler/setup"
53
+ # 1. include the Pedicab module
54
+ require 'pedicab'
55
+
56
+ # 2. create local Pedicab
57
+ @me = Pedicab["me"]
58
+
59
+ if File.exist? 'me.txt'
60
+ # 3. use the me.txt file to predefine context.
61
+ @me[File.read('me.txt')]
62
+ end
63
+
64
+ # 4. start interactive session
65
+ Pry.start
66
+ # ... that's it.
67
+ ```
68
+
69
+ ### Basic Usage
70
+
71
+ ```ruby
72
+ require 'pedicab'
73
+
74
+ # Create a new conversation instance
75
+ ai = Pedicab['my_assistant']
76
+
77
+ # Start a fresh conversation
78
+ response = ai["Hello, how are you?"]
79
+ puts response.out
80
+ # => "I'm doing well, thank you for asking!"
81
+
82
+ # Continue the conversation with context
83
+ response = ai["Tell me about Ruby programming"]
84
+ puts response.out
85
+ # => "Ruby is a dynamic, object-oriented programming language..."
86
+ ```
87
+
88
+ ### Context Management
89
+
90
+ ```ruby
91
+ # Reset conversation context
92
+ ai.reset!
93
+
94
+ # The [] operator starts fresh
95
+ ai["What's the capital of France?"] # New conversation
96
+
97
+ # The << operator continues with context
98
+ ai["What about Germany?"] # Knows we're discussing countries
99
+ ```
100
+
101
+ ### Custom Handlers
102
+
103
+ ```ruby
104
+ # Define custom response handling
105
+ ai.handle do |response|
106
+ puts "AI Response: #{response.out}"
107
+ puts "Processing time: #{response.took}s"
108
+ response
109
+ end
110
+
111
+ ai["Explain quantum computing"]
112
+ # => AI Response: Quantum computing is a revolutionary...
113
+ # => Processing time: 1.23s
114
+ ```
115
+
116
+ ## Core API
117
+
118
+ ### Pedicab::P Class
119
+
120
+ The main interface for AI conversations.
121
+
122
+ #### Methods
123
+
124
+ - `initialize(id)` - Create new conversation instance
125
+ - `reset!` - Clear conversation context and state
126
+ - `[](prompt)` - Start fresh conversation with prompt
127
+ - `<<(prompt)` - Continue conversation with context
128
+ - `handle(&block)` - Set custom response handler
129
+ - `life()` - Total processing time for all requests
130
+
131
+ #### Attributes
132
+
133
+ - `took` - Time taken for last request
134
+ - `prompt` - Last prompt sent
135
+ - `response` - Array of all responses
136
+ - `out` - Last response content
137
+ - `last` - Array of previous prompts
138
+ - `thoughts` - LLM thinking process (if available)
139
+ - `context` - Current conversation context
140
+ - `handler` - Response handler lambda
141
+
142
+ ### Pedicab::Ride Class
143
+
144
+ Low-level LLM interface for direct model interaction.
145
+
146
+ #### Methods
147
+
148
+ - `go(prompt, &block)` - Send prompt to model, optionally process each line
149
+ - `if?(condition, &block)` - Evaluate boolean condition, optionally execute block
150
+ - `reset!` - Clear ride state
151
+ - `[](key)` - Access state value
152
+ - `[]=(key, value)` - Set state value
153
+ - `to_h` - Get state as hash
154
+
155
+ #### State
156
+
157
+ The ride maintains internal state including:
158
+ - `:content` - Model response content
159
+ - `:thought` - Model thinking process
160
+ - `:action` - Current action description
161
+ - `:yes` - Boolean result for conditions
162
+ - `:took` - Processing time
163
+
164
+ ### Module Methods
165
+
166
+ - `Pedicab.[](id)` - Create new conversation instance
167
+ - `Pedicab.models` - List available models in `/models/`
168
+ - `Pedicab.ride(id)` - Create new ride instance
169
+
170
+ ## Advanced Usage
171
+
172
+ ### Conditional Processing
173
+
174
+ ```ruby
175
+ ai = Pedicab['conditional_bot']
176
+
177
+ # Use conditional logic
178
+ if ai.ride.if?("the user wants programming help")
179
+ ai["I'll help you with programming!"]
180
+ else
181
+ ai["How can I assist you today?"]
182
+ end
183
+ ```
184
+
185
+ ### Iterating Over Responses
186
+
187
+ ```ruby
188
+ # Process each line of a response
189
+ ai.ride.go("List 3 programming languages") do |line|
190
+ puts "Language: #{line[:for]}"
191
+ end
192
+ ```
193
+
194
+ ### State Management
195
+
196
+ ```ruby
197
+ ride = Pedicab.ride['stateful_bot']
198
+
199
+ # Store custom state
200
+ ride[:user_preference] = "detailed"
201
+ ride[:last_topic] = "Ruby"
202
+
203
+ # Access state later
204
+ puts ride[:user_preference] # => "detailed"
205
+ puts ride.to_h # => { user_preference: "detailed", last_topic: "Ruby" }
206
+ ```
207
+
208
+ ### Performance Monitoring
209
+
210
+ ```ruby
211
+ ai = Pedicab['monitored_bot']
212
+
213
+ ai["First question"] # Processes
214
+ puts ai.took # Time for last request: 0.85s
215
+
216
+ ai["Follow up question"] # Processes
217
+ puts ai.life # Total time: 1.67s
218
+ ```
219
+
220
+ ## Configuration
221
+
222
+ ### Environment Variables
223
+
224
+ - `MODEL` - Default model name (default: 'qwen')
225
+ - `DEBUG` - Debug level (0=off, 1=basic, 2=verbose)
226
+
227
+ ### Model Management
228
+
229
+ ```ruby
230
+ # List available models
231
+ puts Pedicab.models
232
+ # => ["qwen", "llama2", "mistral"]
233
+
234
+ # Models should be placed in /models/ directory as .gguf files
235
+ # /models/qwen.gguf
236
+ # /models/llama2.gguf
237
+ # /models/mistral.gguf
238
+ ```
239
+
240
+ ## Architecture
241
+
242
+ ### Core Components
243
+
244
+ 1. **Ruby Interface** (`lib/pedicab.rb`)
245
+ - Main API and conversation management
246
+ - Context tracking and state management
247
+ - Elegant operator interface
248
+
249
+ 2. **Ride Layer** (`lib/pedicab/ride.rb`)
250
+ - Low-level LLM communication
251
+ - JSON-based protocol with Python backend
252
+ - State management and error handling
253
+
254
+ 3. **Python Backend** (`lib/pedicab.py`)
255
+ - llama-cpp-python integration
256
+ - Model loading and inference
257
+ - Grammar-based constrained outputs
258
+
259
+ ### Communication Flow
260
+
261
+ ```
262
+ Ruby P Class -> Ride Class -> Python Backend -> LLM Model -> Response
263
+ ```
264
+
265
+ 1. Ruby sends JSON request to Python process
266
+ 2. Python processes with llama-cpp-python
267
+ 3. Response includes content and thinking process
268
+ 4. Ruby parses and manages state
269
+
270
+ ## Error Handling
271
+
272
+ ```ruby
273
+ begin
274
+ ai = Pedicab['test_bot']
275
+ response = ai["Some question"]
276
+ rescue Pedicab::Error => e
277
+ puts "Pedicab error: #{e.message}"
278
+ rescue => e
279
+ puts "Unexpected error: #{e.message}"
280
+ end
281
+ ```
282
+
283
+ ## Debugging
284
+
285
+ Enable debug output:
286
+
287
+ ```ruby
288
+ # In your shell
289
+ export DEBUG=1 # Basic debugging
290
+ export DEBUG=2 # Verbose debugging
291
+
292
+ # Or in Ruby
293
+ ENV['DEBUG'] = '1'
294
+ ```
295
+
296
+ Debug output includes:
297
+ - Action descriptions
298
+ - Processing times
299
+ - Full state information
300
+ - Error details
301
+
302
+ ## Performance Tips
303
+
304
+ 1. **Model Selection**: Choose appropriate model size for your use case
305
+ 2. **Context Management**: Reset conversations when starting new topics
306
+ 3. **Batch Processing**: Use the iteration methods for list responses
307
+ 4. **Monitoring**: Track processing times with `took` and `life` methods
308
+
309
+ ## Contributing
310
+
311
+ 1. Fork the repository
312
+ 2. Create a feature branch
313
+ 3. Make your changes
314
+ 4. Add tests if applicable
315
+ 5. Submit a pull request
316
+
317
+ ## License
318
+
319
+ MIT License - see LICENSE file for details.
320
+
321
+ ## Changelog
322
+
323
+ ### Version 0.3.2
10
324
 
11
- $ bundle add pedicab
325
+ - Simplified core architecture
326
+ - Removed auxiliary modules for focus on essential mechanics
327
+ - Improved performance and reduced dependencies
328
+ - Enhanced error handling and debugging capabilities
12
329
 
13
- If bundler is not being used to manage dependencies, install the gem by executing:
330
+ ### Previous Versions
14
331
 
15
- $ gem install pedicab
332
+ See git history for detailed changelog.
16
333
 
17
- ## Usage
334
+ ## Support
18
335
 
19
- What to say?
336
+ For issues and questions:
337
+ - GitHub Issues: https://github.com/xorgnak/pedicab/issues
338
+ - Documentation: https://github.com/xorgnak/pedicab
20
339
 
21
- Oh, the gem!? okay. Here's the short fast and loud. "AI" has been around since before buddy holly. Not citiong sources. Short-fast-n-lould is their're a tech you should "understand"; no judgment.
340
+ ---
22
341
 
23
- usage: @me = Pedicab['me']
342
+ *Pedicab: A better way to go with local LLMs*
@@ -0,0 +1,27 @@
1
+ module Pedicab
2
+ module City
3
+ class C
4
+ attr_accessor :data
5
+ def initialize(i)
6
+ @id = i
7
+ @data = {}
8
+ end
9
+
10
+ def to_markdown
11
+ @carpet.to_markdown(@data)
12
+ end
13
+
14
+ def to_html
15
+ @carpet.to_html(@data)
16
+ end
17
+ end
18
+
19
+ def self.[](k)
20
+ C.new(k)
21
+ end
22
+ end
23
+
24
+ def self.city
25
+ City
26
+ end
27
+ end
data/lib/pedicab/ride.rb CHANGED
@@ -1,109 +1,88 @@
1
1
  module Pedicab
2
- @@Ride = Hash.new { |h,k| h[k] = Ride.new(k) }
3
2
  class Ride
4
- attr_accessor :state, :action, :goal, :ride, :info, :tale, :path, :model
5
- def initialize k
3
+ attr_accessor :state, :model
4
+ def initialize(k)
6
5
  @id = k
7
- @model = 'model'
6
+ @model = ENV['MODEL'] || 'qwen'
8
7
  reset!
9
8
  end
9
+
10
10
  def reset!
11
11
  @state = {}
12
- @ride = []
13
- @path = []
14
- @info = ""
15
12
  end
16
-
17
- ##
18
- # tell the "tale" of the ride.
19
- def tale
20
- a = []
21
- if @ride.length > 0
22
- h = @ride[-1]
23
- @ride.shift
24
- return %[#{h[:input]}\n#{h[:output]}\n]
13
+
14
+ def go(prompt, &b)
15
+ @state[:action] = "respond: #{prompt}"
16
+ rider(role: 'user', model: @model, content: prompt)
17
+ def each &b
18
+ @state[:content].split("\n").each { |e| @state[:for] = e; b.call(self) }
19
+ end
20
+ if block_given?
21
+ b.call(self)
25
22
  else
26
- return ""
23
+ @state[:content]
27
24
  end
28
25
  end
29
-
30
- ##
31
- # process input.
32
- def go i
33
- @path << "go #{i}"
34
- @state[:action] = "go"
35
- @state[:input] = i
36
- # puts %[#===============[work]>\n#{i}\n#===============[work]];
37
- Pedicab.on[:work].call(self)
38
- @state[:output] = rider(role: 'user', model: 'work', content: "#{@info}#{tale}#{i}")
39
- @ride << @state
40
- @state[:output].gsub(/\n+/, "\n")
41
- end
42
-
43
- ##
44
- # process condition
45
- def go? i
46
- @path << "go? #{i}"
47
- @state[:action] = "go?"
48
- @state[:input] = i
26
+ def if?(condition, &b)
27
+ @state[:action] = "gate: #{condition}"
49
28
  @state[:yes] = false
50
- # puts %[#>>>>>[gate]>\n#{i}\n#>>>>>[gate]];
51
- Pedicab.on[:gate].call(self)
52
- if rider(role: 'system', model: 'gate', content: %[#{@info}#{tale}if the following statement is true respond 'yes', otherwise respond 'no':\n#{i}], response: 'bool' ) == 'yes'
53
- @state[:yes] = true
54
- Pedicab.on[:go].call(self)
29
+ gate_prompt = "If the following statement is true respond 'yes', otherwise respond 'no':\n#{condition}"
30
+ rider(role: 'system', model: @model, content: gate_prompt)
31
+ @state[:yes] = (@state[:content] == 'yes')
32
+ if block_given? && @state[:yes] == true
33
+ b.call(self)
34
+ else
35
+ @state[:yes]
55
36
  end
56
- @ride << @state
57
- @state[:yes]
58
37
  end
59
-
60
- ##
61
- # process list
62
- def go! i, p, &b
63
- @path << "with #{i} of #{p}"
64
- @state[:action] = "go! fork"
65
- @state[:input] = %[#{@info}#{tale}List only #{i} unnumbered without repeating yourself:\n#{p}]
66
- Pedicab.on[:fork].call(self)
67
- @state[:list] = rider(role: 'user', model: 'fork', content: @state[:input]).split("\n").uniq.map { |e|
68
- if e.strip.length > 0;
69
- # puts %[#----------[fork]>\nfor: #{i}\nin: #{p}\nat: #{e}\n#----------[fork]];
70
- Pedicab.on[:with].call(self);
71
- b.call(e.strip);
72
- end;
73
- }
74
- @ride << @state
75
- @state[:list]
38
+
39
+ def [] k
40
+ @state[k]
41
+ end
42
+
43
+ def []= k,v
44
+ @state[k] = v
45
+ end
46
+
47
+ def to_h
48
+ @state
76
49
  end
77
50
 
78
51
  private
79
52
 
80
- def rider h={}, &b
53
+ def rider(h = {})
81
54
  @state[:content] = "I don't know."
82
- # puts %[#====[ #{@ride.length} ][#{@state[:action]}]\n#{@state[:input]}]
83
- Pedicab.on[:before].call(self)
55
+ if ENV['DEBUG'].to_i > 1
56
+ puts "# #{@state[:action]}"
57
+ end
58
+
84
59
  @state[:took] = Benchmark.realtime do
85
60
  Open3.popen3("pedicab /models/#{h[:model]}.gguf") do |stdin, stdout, stderr, wait_thread|
86
- x = lambda { stdin.puts(JSON.generate(h)); stdout.gets }
87
- begin
88
- xx = x.call()
89
- if "#{xx}".strip.length > 0
90
- @state[:content] = JSON.parse("#{xx}")['content']
91
- end
92
- rescue => e
93
- @state[:content] = "Error: #{e}"
94
- end
61
+ stdin.puts(JSON.generate(h))
62
+ response = stdout.gets
63
+
64
+ if response && response.strip.length > 0
65
+ content = JSON.parse(response)['content']
66
+ if match = /<think>([\s\S]*)<\/think>([\s\S]*)/.match(content)
67
+ @state[:thought] = match[1].strip.gsub(/\n+/, "\n").gsub('\\n', "\n")
68
+ @state[:content] = match[2].strip.gsub(/\n+/, "\n").gsub('\\n', "\n")
69
+ else
70
+ @state[:thought] = h[:content].gsub('\\n', "\n")
71
+ @state[:content] = content.strip.gsub(/\n+/, "\n").gsub('\\n', "\n")
72
+ end
73
+ end
74
+ rescue => e
75
+ @state[:content] = "Error: #{e}"
95
76
  end
96
77
  end
97
- Pedicab.on[:after].call(self)
98
- # puts %[#====[ #{@ride.length} ] took: #{@state[:took]} seconds.\n#{@state[:content].gsub(/\n+/,"\n")}]
99
- if block_given?
100
- return b.call(self).gsub(/\n+/, "\n")
101
- else
102
- return @state[:content].gsub(/\n+/, "\n")
78
+
79
+ if ENV['DEBUG'].to_i > 0
80
+ @state.each_pair { |k, v| puts "#{k}: #{v}" }
103
81
  end
104
82
  end
105
83
  end
106
- def self.ride
107
- @@Ride
84
+
85
+ def self.ride(k)
86
+ Ride.new(k)
108
87
  end
109
88
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pedicab
4
- VERSION = "0.3.1"
4
+ VERSION = "0.3.3"
5
5
  end
data/lib/pedicab.py CHANGED
@@ -5,19 +5,15 @@ import json
5
5
 
6
6
  llm = Llama(
7
7
  model_path=sys.argv[1],
8
- # model_path="/models/planner.gguf",
9
- # model_path="/models/smollm.gguf",
10
8
  verbose=False,
11
9
  errors="ignore",
12
10
  n_ctx=8192
13
11
  )
14
12
 
15
- __sys = "A conversation between a curious Human and an all knowing Assistant who responds only to what was asked in simple terms and never repeats itself.\n\nWhen asked to respond with a single value, respond with only that value.\nWhen asked to list items, respond with the list of items sepearated by a newline only and again - never repeat yourself.\nWhen asked to respond with a mathmatical formula, respond with only the bare formula in the format necessary to be evaluated by the ruby eqn gem.\nOtherwise, respond with a single level outline and NEVER repeat yourself."
13
+ __sys = "A conversation between a curious Human and an all knowing Assistant who responds only to what was asked in simple terms and never repeats itself.\n\nWhen asked to respond with a single value, respond with only that value.\nWhen asked to respond with an empty list, respond only with an empty string.\nWhen asked to list items respond with the list of items sepearated by a newline only and again - never repeat yourself.\nWhen asked to respond with a mathmatical formula, respond with only the bare formula in the format necessary to be evaluated by the ruby eqn gem.\nOtherwise, respond with a single level outline and NEVER repeat yourself."
16
14
  #__sys = "A conversation between a Human and an all knowing Llama."
17
15
 
18
-
19
- prompt = __sys
20
- convo = [{ "role": "system", "content": prompt }]
16
+ convo = [{ "role": "system", "content": __sys }]
21
17
  grammars = {
22
18
  "bool": 'root ::= ("yes" | "no")',
23
19
  "number": "root ::= (\"-\"?([0-9]+))",
@@ -30,8 +26,7 @@ while True:
30
26
  user = json.loads(input(""))
31
27
 
32
28
  if (user["role"] == "assistant"):
33
- convo.append({ "role": "user", "content": user["content"] })
34
- convo.append({ "role": "assistant", "content": user["response"] })
29
+ convo = [{ "role": "system", "content": user["content"] }]
35
30
 
36
31
  if (user["role"] == "system"):
37
32
  output = llm( user["content"], stop=["\n"], echo=False, temperature=0.0, grammar=LlamaGrammar.from_string(grammars[user['response']]))["choices"][0]["text"]