zuzu 0.0.1-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zuzu
4
+ # Manages the llamafile subprocess lifecycle.
5
+ class LlamafileManager
6
+ STARTUP_TIMEOUT = 60 # seconds — models can take a while
7
+ SHUTDOWN_TIMEOUT = 5
8
+
9
+ attr_reader :pid
10
+
11
+ def initialize(path: Zuzu.config.llamafile_path, port: Zuzu.config.port)
12
+ @path = path
13
+ @port = port
14
+ @pid = nil
15
+ @client = LlmClient.new(port: @port)
16
+ end
17
+
18
+ def start!
19
+ raise "llamafile already running (pid=#{@pid})" if running?
20
+ raise "llamafile not found: #{@path}" unless File.exist?(@path.to_s)
21
+
22
+ log_file = File.expand_path('llama.log', File.dirname(@path))
23
+ @pid = Process.spawn(
24
+ @path, '--server', '--port', @port.to_s, '--nobrowser',
25
+ out: log_file, err: log_file
26
+ )
27
+ Process.detach(@pid)
28
+ wait_for_ready
29
+ @pid
30
+ end
31
+
32
+ def stop!
33
+ return unless @pid
34
+ Process.kill('TERM', @pid) rescue nil
35
+ deadline = Time.now + SHUTDOWN_TIMEOUT
36
+ while Time.now < deadline
37
+ return (@pid = nil) unless alive?(@pid)
38
+ sleep 0.25
39
+ end
40
+ Process.kill('KILL', @pid) rescue nil
41
+ @pid = nil
42
+ end
43
+
44
+ def running?
45
+ @pid && alive?(@pid)
46
+ end
47
+
48
+ private
49
+
50
+ def wait_for_ready
51
+ deadline = Time.now + STARTUP_TIMEOUT
52
+ until @client.alive?
53
+ if Time.now > deadline
54
+ stop!
55
+ raise "llamafile failed to start within #{STARTUP_TIMEOUT}s"
56
+ end
57
+ sleep 1
58
+ end
59
+ end
60
+
61
+ def alive?(pid)
62
+ Process.kill(0, pid)
63
+ true
64
+ rescue Errno::ESRCH, Errno::EPERM, Errno::EINVAL
65
+ false
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'openssl'
5
+ require 'json'
6
+ require 'uri'
7
+
8
+ module Zuzu
9
+ # HTTP client for llamafile's OpenAI-compatible API.
10
+ class LlmClient
11
+ def initialize(host: '127.0.0.1', port: Zuzu.config.port)
12
+ @host = host
13
+ @port = port
14
+ end
15
+
16
+ def chat(messages, temperature: 0.1)
17
+ body = {
18
+ model: Zuzu.config.model,
19
+ messages: messages,
20
+ temperature: temperature
21
+ }
22
+ data = post_json('/v1/chat/completions', body)
23
+ msg = data.dig('choices', 0, 'message')
24
+ strip_eos(msg)
25
+ end
26
+
27
+ def alive?
28
+ uri = URI("http://#{@host}:#{@port}/v1/models")
29
+ Net::HTTP.get_response(uri).is_a?(Net::HTTPSuccess)
30
+ rescue StandardError
31
+ false
32
+ end
33
+
34
+ def stream(messages, &block)
35
+ body = { model: Zuzu.config.model, messages: messages, stream: true }
36
+ uri = URI("http://#{@host}:#{@port}/v1/chat/completions")
37
+ req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
38
+ req.body = JSON.generate(body)
39
+
40
+ Net::HTTP.start(uri.host, uri.port) do |http|
41
+ http.request(req) do |response|
42
+ response.read_body do |chunk|
43
+ chunk.each_line do |line|
44
+ content = parse_sse(line)
45
+ block.call(content) if content
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def post_json(path, body)
55
+ uri = URI("http://#{@host}:#{@port}#{path}")
56
+ req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
57
+ req.body = JSON.generate(body)
58
+ res = Net::HTTP.start(uri.host, uri.port, read_timeout: 120,
59
+ use_ssl: false) { |http| http.request(req) }
60
+ JSON.parse(res.body)
61
+ end
62
+
63
+ def strip_eos(msg)
64
+ return msg unless msg.is_a?(Hash) && msg['content'].is_a?(String)
65
+ msg['content'] = msg['content']
66
+ .gsub(/<\/?s>/, '')
67
+ .gsub(%r{\[/?INST\]}, '')
68
+ .strip
69
+ msg
70
+ end
71
+
72
+ def parse_sse(line)
73
+ line = line.strip
74
+ return nil if line.empty? || line == 'data: [DONE]'
75
+ return nil unless line.start_with?('data: ')
76
+ json = JSON.parse(line.sub('data: ', ''))
77
+ json.dig('choices', 0, 'delta', 'content')
78
+ rescue JSON::ParserError
79
+ nil
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Zuzu
6
+ # Conversation memory backed by the messages table.
7
+ class Memory
8
+ def initialize(store = nil)
9
+ @store = store || Store.new
10
+ bootstrap
11
+ end
12
+
13
+ def append(role, content)
14
+ @store.execute(
15
+ "INSERT INTO messages (role, content, created_at) VALUES (?, ?, ?)",
16
+ [role.to_s, content.to_s, (Time.now.to_f * 1000).to_i]
17
+ )
18
+ end
19
+
20
+ def recent(limit = 20)
21
+ @store.query_all(
22
+ "SELECT role, content FROM messages ORDER BY id DESC LIMIT ?", [limit]
23
+ ).reverse
24
+ end
25
+
26
+ def clear
27
+ @store.execute("DELETE FROM messages")
28
+ end
29
+
30
+ def context_for_llm(limit = 20)
31
+ recent(limit).map { |m| { 'role' => m['role'], 'content' => m['content'] } }
32
+ end
33
+
34
+ private
35
+
36
+ def bootstrap
37
+ @store.execute(<<~SQL)
38
+ CREATE TABLE IF NOT EXISTS messages (
39
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
40
+ role TEXT NOT NULL,
41
+ content TEXT NOT NULL,
42
+ created_at INTEGER NOT NULL
43
+ )
44
+ SQL
45
+ end
46
+ end
47
+ end
data/lib/zuzu/store.rb ADDED
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jdbc/sqlite3'
4
+ require 'fileutils'
5
+ Jdbc::SQLite3.load_driver
6
+
7
+ module Zuzu
8
+ # SQLite query layer used by AgentFS and Memory.
9
+ # One shared JDBC connection per db_path, guarded by a Mutex.
10
+ class Store
11
+ attr_reader :db_path
12
+
13
+ def initialize(db_path = Zuzu.config.db_path)
14
+ @db_path = db_path
15
+ @mutex = Mutex.new
16
+ end
17
+
18
+ def connection
19
+ @connection ||= begin
20
+ FileUtils.mkdir_p(File.dirname(@db_path))
21
+ conn = Java::OrgSqlite::JDBC.new.connect(
22
+ "jdbc:sqlite:#{@db_path}", java.util.Properties.new
23
+ )
24
+ # Enable WAL for better concurrent read performance
25
+ stmt = conn.create_statement
26
+ stmt.execute_update("PRAGMA journal_mode=WAL")
27
+ stmt.close
28
+ conn
29
+ end
30
+ end
31
+
32
+ def execute(sql, params = [])
33
+ @mutex.synchronize do
34
+ ps = connection.prepare_statement(sql)
35
+ params.each_with_index { |v, i| ps.set_object(i + 1, v) }
36
+ ps.execute_update
37
+ ensure
38
+ ps&.close
39
+ end
40
+ end
41
+
42
+ def query_all(sql, params = [])
43
+ @mutex.synchronize do
44
+ ps = connection.prepare_statement(sql)
45
+ params.each_with_index { |v, i| ps.set_object(i + 1, v) }
46
+ rs = ps.execute_query
47
+ meta = rs.meta_data
48
+ cols = (1..meta.column_count).map { |c| meta.get_column_name(c) }
49
+ rows = []
50
+ while rs.next
51
+ row = {}
52
+ cols.each { |col| row[col] = rs.get_object(col) }
53
+ rows << row
54
+ end
55
+ rows
56
+ ensure
57
+ rs&.close
58
+ ps&.close
59
+ end
60
+ end
61
+
62
+ def query_one(sql, params = [])
63
+ query_all(sql, params).first
64
+ end
65
+
66
+ def last_insert_id
67
+ @mutex.synchronize do
68
+ stmt = connection.create_statement
69
+ rs = stmt.execute_query("SELECT last_insert_rowid()")
70
+ rs.next ? rs.get_long(1) : nil
71
+ ensure
72
+ rs&.close
73
+ stmt&.close
74
+ end
75
+ end
76
+
77
+ def close
78
+ @mutex.synchronize do
79
+ @connection&.close
80
+ @connection = nil
81
+ end
82
+ rescue StandardError
83
+ # Best-effort close — don't raise during shutdown
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Zuzu
6
+ # Global registry of callable tools for the agent loop.
7
+ module ToolRegistry
8
+ Tool = Struct.new(:name, :description, :schema, :block, keyword_init: true)
9
+
10
+ @tools = {}
11
+
12
+ class << self
13
+ def register(name, description, schema, &block)
14
+ @tools[name.to_s] = Tool.new(
15
+ name: name.to_s, description: description,
16
+ schema: schema, block: block
17
+ )
18
+ end
19
+
20
+ def tools = @tools.values
21
+ def find(name) = @tools[name.to_s]
22
+
23
+ def to_openai_schema
24
+ tools.map do |t|
25
+ { type: 'function', function: { name: t.name, description: t.description, parameters: t.schema } }
26
+ end
27
+ end
28
+
29
+ def execute(name, args, agent_fs)
30
+ tool = find(name) or return "Error: unknown tool '#{name}'"
31
+ started = Time.now.to_f
32
+ output = begin
33
+ tool.block.call(args, agent_fs)
34
+ rescue StandardError => e
35
+ "Error: #{e.message}"
36
+ end
37
+ finished = Time.now.to_f
38
+ agent_fs.record_tool_call(name, JSON.generate(args), output.to_s, started, finished)
39
+ output.to_s
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ Zuzu::ToolRegistry.register(
4
+ 'read_file', 'Read the contents of a file from the sandbox filesystem.',
5
+ { type: 'object', properties: { path: { type: 'string', description: 'Absolute path' } }, required: ['path'] }
6
+ ) { |args, fs| fs.read_file(args['path']) || "File not found: #{args['path']}" }
7
+
8
+ Zuzu::ToolRegistry.register(
9
+ 'write_file', 'Write content to a file in the sandbox filesystem.',
10
+ { type: 'object', properties: {
11
+ path: { type: 'string', description: 'Absolute path' },
12
+ content: { type: 'string', description: 'Content to write' }
13
+ }, required: %w[path content] }
14
+ ) do |args, fs|
15
+ fs.write_file(args['path'], args['content'])
16
+ "Written #{args['content'].to_s.bytesize} bytes to #{args['path']}"
17
+ end
18
+
19
+ Zuzu::ToolRegistry.register(
20
+ 'list_directory', 'List entries in a directory.',
21
+ { type: 'object', properties: { path: { type: 'string', description: 'Directory path (default "/")' } }, required: [] }
22
+ ) do |args, fs|
23
+ entries = fs.list_dir(args['path'] || '/')
24
+ entries.empty? ? '(empty)' : entries.join("\n")
25
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ Zuzu::ToolRegistry.register(
4
+ 'run_command', 'Run a sandboxed command against AgentFS (NOT the host filesystem).',
5
+ { type: 'object', properties: { command: { type: 'string', description: 'Sandboxed command' } }, required: ['command'] }
6
+ ) do |args, fs|
7
+ cmd = args['command'].to_s.strip
8
+ parts = cmd.split(/\s+/)
9
+ base = parts[0]
10
+
11
+ case base
12
+ when 'ls'
13
+ path = parts[1] || '/'
14
+ stat = fs.stat(path)
15
+ if stat && stat['type'] == 'file'
16
+ "#{path} (file, #{stat['size']} bytes)"
17
+ else
18
+ entries = fs.list_dir(path)
19
+ entries.empty? ? '(empty directory)' : entries.join("\n")
20
+ end
21
+ when 'cat'
22
+ path = parts[1]
23
+ path ? (fs.read_file(path) || "File not found in AgentFS: #{path}") : 'Usage: cat <path>'
24
+ when 'pwd'
25
+ '/'
26
+ when 'echo'
27
+ parts[1..].join(' ')
28
+ else
29
+ "Error: '#{base}' is not available in the AgentFS sandbox. Supported: ls, cat, pwd, echo."
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'openssl'
5
+ require 'uri'
6
+
7
+ Zuzu::ToolRegistry.register(
8
+ 'http_get', 'Fetch a URL via HTTP GET (truncated to 8 KB).',
9
+ { type: 'object', properties: { url: { type: 'string', description: 'URL to fetch' } }, required: ['url'] }
10
+ ) do |args, _fs|
11
+ uri = URI.parse(args['url'].to_s)
12
+ raise ArgumentError, 'Only http/https supported' unless %w[http https].include?(uri.scheme)
13
+ res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https',
14
+ open_timeout: 10, read_timeout: 15,
15
+ verify_mode: OpenSSL::SSL::VERIFY_NONE) { |h| h.get(uri.request_uri, 'User-Agent' => 'Zuzu/1.0') }
16
+ res.body.to_s.encode('UTF-8', invalid: :replace, undef: :replace).slice(0, 8192)
17
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zuzu
4
+ VERSION = "0.2.0"
5
+ end
data/lib/zuzu.rb ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Zuzu — JRuby desktop framework for local-first agentic apps.
4
+ #
5
+ # Author: Abhishek Parolkar <apac.abhi@gmail.com>
6
+ # License: MIT
7
+ # Homepage: https://github.com/parolkar/zuzu
8
+
9
+ require 'zuzu/version'
10
+ require 'zuzu/config'
11
+ require 'zuzu/store'
12
+ require 'zuzu/agent_fs'
13
+ require 'zuzu/memory'
14
+ require 'zuzu/llm_client'
15
+ require 'zuzu/llamafile_manager'
16
+ require 'zuzu/tool_registry'
17
+ require 'zuzu/tools/file_tool'
18
+ require 'zuzu/tools/shell_tool'
19
+ require 'zuzu/tools/web_tool'
20
+ require 'zuzu/agent'
21
+ require 'zuzu/channels/base'
22
+ require 'zuzu/channels/in_app'
23
+ require 'zuzu/channels/whatsapp'
24
+ require 'zuzu/app'
data/templates/app.rb ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zuzu'
4
+
5
+ # ── Configure ────────────────────────────────────────────────────
6
+ # Paths are automatically expanded to absolute, so relative paths work fine.
7
+ Zuzu.configure do |c|
8
+ c.app_name = 'My Assistant'
9
+ c.llamafile_path = File.join(__dir__, 'models', 'your-model.llamafile')
10
+ c.db_path = File.join(__dir__, '.zuzu', 'zuzu.db')
11
+ c.port = 8080
12
+ # c.channels = ['whatsapp']
13
+ end
14
+
15
+ # ── Custom Tools ─────────────────────────────────────────────────
16
+ Zuzu::ToolRegistry.register(
17
+ 'greet', 'Greet a user by name.',
18
+ { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }
19
+ ) { |args, _fs| "Hello, #{args['name']}!" }
20
+
21
+ # ── Launch ───────────────────────────────────────────────────────
22
+ Zuzu::App.launch!(use_llamafile: true)
metadata ADDED
@@ -0,0 +1,137 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zuzu
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: java
6
+ authors:
7
+ - Abhishek Parolkar
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: glimmer-dsl-swt
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '4.30'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '4.30'
26
+ - !ruby/object:Gem::Dependency
27
+ name: jdbc-sqlite3
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.46'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.46'
40
+ - !ruby/object:Gem::Dependency
41
+ name: webrick
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '1.7'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '1.7'
54
+ - !ruby/object:Gem::Dependency
55
+ name: bigdecimal
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: logger
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ description: Zuzu is a framework for building local-first, privacy-respecting desktop
83
+ AI assistants using JRuby, Glimmer DSL for SWT, SQLite, and llamafile.
84
+ email:
85
+ - abhishek@parolkar.com
86
+ executables:
87
+ - zuzu
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - LICENSE
92
+ - README.md
93
+ - bin/setup
94
+ - bin/zuzu
95
+ - lib/zuzu.rb
96
+ - lib/zuzu/agent.rb
97
+ - lib/zuzu/agent_fs.rb
98
+ - lib/zuzu/app.rb
99
+ - lib/zuzu/channels/base.rb
100
+ - lib/zuzu/channels/in_app.rb
101
+ - lib/zuzu/channels/whatsapp.rb
102
+ - lib/zuzu/config.rb
103
+ - lib/zuzu/llamafile_manager.rb
104
+ - lib/zuzu/llm_client.rb
105
+ - lib/zuzu/memory.rb
106
+ - lib/zuzu/store.rb
107
+ - lib/zuzu/tool_registry.rb
108
+ - lib/zuzu/tools/file_tool.rb
109
+ - lib/zuzu/tools/shell_tool.rb
110
+ - lib/zuzu/tools/web_tool.rb
111
+ - lib/zuzu/version.rb
112
+ - templates/app.rb
113
+ homepage: https://github.com/parolkar/zuzu
114
+ licenses:
115
+ - MIT
116
+ metadata:
117
+ homepage_uri: https://github.com/parolkar/zuzu
118
+ source_code_uri: https://github.com/parolkar/zuzu
119
+ bug_tracker_uri: https://github.com/parolkar/zuzu/issues
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: 3.1.0
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubygems_version: 3.7.2
135
+ specification_version: 4
136
+ summary: Local-first agentic desktop apps with JRuby.
137
+ test_files: []