simple-rag-zc 0.1.0 → 0.1.2
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 +4 -4
- data/README.md +2 -2
- data/example_config.json +5 -2
- data/exe/public/q.html +1 -70
- data/exe/public/setup.html +136 -0
- data/exe/run-index +1 -2
- data/exe/run-server +111 -100
- data/exe/run-setup +55 -0
- data/lib/simple_rag/version.rb +1 -1
- data/lib/simple_rag.rb +1 -2
- data/llm/embedding.rb +0 -0
- data/llm/http.rb +0 -0
- data/llm/llm.rb +57 -0
- data/llm/ollama.rb +24 -4
- data/llm/openai.rb +7 -14
- data/readers/check-reader.rb +0 -0
- data/readers/journal.rb +69 -0
- data/readers/note.rb +0 -0
- data/readers/reader.rb +14 -5
- data/readers/text.rb +16 -3
- data/server/cache.rb +0 -0
- data/server/discuss.rb +1 -1
- data/server/retriever.rb +14 -9
- data/server/synthesizer.rb +1 -1
- data/storage/mem.rb +0 -0
- metadata +24 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 906d584b90596bde4fef5efef3f82cceb300284705d6c33023f84dff903f4d2e
|
4
|
+
data.tar.gz: 8d1c292cefc14246e918e06d44cdef48bf31548fd5f1aeaa375b527ee4603458
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 94d4c13cd41807bf416882f8241f2ea103d00bacf6662b9b901c92a2f4e65463bdae64d204d955850fcfa9703f36ade459a5d23df837be430e4b7373fa70aa1a
|
7
|
+
data.tar.gz: 79f7b78fa5f363b0c5c430f6a4d2af78485cf68b533e26006ef1548a535397a5ffed7e8f19f4d7a6602de1f9ccfdfe73c0f50687fe279796b4ab53d271ad7123
|
data/README.md
CHANGED
@@ -18,11 +18,11 @@ To release a new version to [RubyGems](https://rubygems.org), run:
|
|
18
18
|
|
19
19
|
```bash
|
20
20
|
gem build simple-rag.gemspec
|
21
|
-
gem push simple-rag-$(ruby -Ilib -e 'require "simple_rag/version"; puts SimpleRag::VERSION').gem
|
21
|
+
gem push simple-rag-zc-$(ruby -Ilib -e 'require "simple_rag/version"; puts SimpleRag::VERSION').gem
|
22
22
|
```
|
23
23
|
|
24
24
|
Install the gem directly:
|
25
25
|
|
26
26
|
```bash
|
27
|
-
gem install simple-rag
|
27
|
+
gem install simple-rag-zc
|
28
28
|
```
|
data/example_config.json
CHANGED
@@ -15,7 +15,10 @@
|
|
15
15
|
"reader": "text",
|
16
16
|
"threshold": 0.3,
|
17
17
|
"dir": "D:\\Studies\\tmp\\learning",
|
18
|
-
"out": "D:\\Studies\\tmp\\learning
|
18
|
+
"out": "D:\\Studies\\tmp\\learning\\learning.dt",
|
19
|
+
"nameMatch": "talks-*.md",
|
20
|
+
"url": "",
|
21
|
+
"searchDefault": false
|
19
22
|
}
|
20
23
|
]
|
21
|
-
}
|
24
|
+
}
|
data/exe/public/q.html
CHANGED
@@ -114,7 +114,7 @@
|
|
114
114
|
checkbox.type = 'checkbox';
|
115
115
|
checkbox.id = item.name;
|
116
116
|
checkbox.name = item.name;
|
117
|
-
checkbox.checked =
|
117
|
+
checkbox.checked = !!item.searchDefault;
|
118
118
|
|
119
119
|
const label = document.createElement('label');
|
120
120
|
label.htmlFor = item.name;
|
@@ -228,75 +228,6 @@
|
|
228
228
|
.catch(error => console.error('Error performing agent search:', error));
|
229
229
|
}
|
230
230
|
|
231
|
-
function performAgentSearch() {
|
232
|
-
const query = searchInput.value;
|
233
|
-
const configExperiment = configExperimentCheckbox.checked
|
234
|
-
const checkedPaths = Array.from(pathsList.querySelectorAll('input[type="checkbox"]:checked'))
|
235
|
-
.map(checkbox => checkbox.name);
|
236
|
-
|
237
|
-
fetch('http://localhost:4567/q_plus', {
|
238
|
-
method: 'POST',
|
239
|
-
headers: {
|
240
|
-
'Content-Type': 'application/json',
|
241
|
-
},
|
242
|
-
body: JSON.stringify({
|
243
|
-
q: query,
|
244
|
-
paths: checkedPaths,
|
245
|
-
experiment: configExperiment,
|
246
|
-
})
|
247
|
-
})
|
248
|
-
.then(response => response.json())
|
249
|
-
.then(resp => {
|
250
|
-
responseContainer.innerHTML = '';
|
251
|
-
|
252
|
-
if (!!resp.expanded) {
|
253
|
-
const div = document.createElement('div');
|
254
|
-
div.className = 'response-item';
|
255
|
-
div.style.backgroundColor = textToLightColor("expanded");
|
256
|
-
div.innerHTML = `<div><strong>Expanded Query:</strong> ${resp.expanded}</div>`;
|
257
|
-
responseContainer.appendChild(div);
|
258
|
-
}
|
259
|
-
|
260
|
-
if (resp.variants && resp.variants.length > 0) {
|
261
|
-
const div = document.createElement('div');
|
262
|
-
div.className = 'response-item';
|
263
|
-
div.style.backgroundColor = textToLightColor("variants");
|
264
|
-
div.innerHTML = `
|
265
|
-
<div><strong>Variants:</strong> ${resp.variants.join(', ')}</div>
|
266
|
-
`;
|
267
|
-
responseContainer.appendChild(div);
|
268
|
-
}
|
269
|
-
|
270
|
-
if (!!resp.eval) {
|
271
|
-
const div = document.createElement('div');
|
272
|
-
div.className = 'response-item';
|
273
|
-
div.style.backgroundColor = textToLightColor("experiment");
|
274
|
-
div.innerHTML = `
|
275
|
-
<div class="markdown-content">${marked.parse(resp.eval)}</div>
|
276
|
-
`;
|
277
|
-
responseContainer.appendChild(div);
|
278
|
-
}
|
279
|
-
|
280
|
-
resp.data.forEach(item => {
|
281
|
-
const div = document.createElement('div');
|
282
|
-
div.className = 'response-item';
|
283
|
-
div.style.backgroundColor = textToLightColor(item.lookup);
|
284
|
-
div.dataset.note = item.text;
|
285
|
-
div.innerHTML = `
|
286
|
-
<div><strong>Path:</strong> <a href="${item.url}">${item.id}</a></div>
|
287
|
-
<div><strong>Score:</strong> ${item.score}</div>
|
288
|
-
<div class="markdown-content">${marked.parse(item.text)}</div>
|
289
|
-
`;
|
290
|
-
const btn = document.createElement('button');
|
291
|
-
btn.className = 'discuss-button';
|
292
|
-
btn.textContent = 'Discuss';
|
293
|
-
btn.addEventListener('click', () => discussCard(div));
|
294
|
-
div.appendChild(btn);
|
295
|
-
responseContainer.appendChild(div);
|
296
|
-
});
|
297
|
-
})
|
298
|
-
.catch(error => console.error('Error performing agent search:', error));
|
299
|
-
}
|
300
231
|
|
301
232
|
function textToLightColor(text) {
|
302
233
|
// Generate a hash from the text
|
@@ -0,0 +1,136 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<title>Setup SimpleRag</title>
|
6
|
+
<style>
|
7
|
+
body { font-family: Arial, sans-serif; margin: 20px; }
|
8
|
+
.path-item { margin-bottom: 20px; padding: 15px; border: 1px solid #ccc; }
|
9
|
+
.path-item input[type="text"], .path-item select { width: 300px; margin-bottom: 10px; }
|
10
|
+
.path-item label { display: block; margin-bottom: 5px; }
|
11
|
+
</style>
|
12
|
+
</head>
|
13
|
+
<body>
|
14
|
+
<h1>Setup SimpleRag Config</h1>
|
15
|
+
<form id="config-form">
|
16
|
+
<h2>Paths</h2>
|
17
|
+
<div id="paths"></div>
|
18
|
+
<button type="button" onclick="addPath()">Add Path</button>
|
19
|
+
<h2>Chat</h2>
|
20
|
+
<label>Provider: <input id="chat_provider" value="openai"></label><br>
|
21
|
+
<label>URL: <input id="chat_url" value=""></label><br>
|
22
|
+
<label>Model: <input id="chat_model" value="gpt-3.5-turbo-16k"></label>
|
23
|
+
<h2>Embedding</h2>
|
24
|
+
<label>Provider: <input id="emb_provider" value="openai"></label><br>
|
25
|
+
<label>URL: <input id="emb_url" value=""></label><br>
|
26
|
+
<label>Model: <input id="emb_model" value="text-embedding-3-small"></label>
|
27
|
+
<br><br>
|
28
|
+
<button type="submit">Save</button>
|
29
|
+
</form>
|
30
|
+
<script>
|
31
|
+
let READERS = [];
|
32
|
+
|
33
|
+
function fillReaderSelect(select, value){
|
34
|
+
select.innerHTML = READERS.map(r=>`<option value="${r}">${r}</option>`).join('');
|
35
|
+
if(value){ select.value = value; }
|
36
|
+
}
|
37
|
+
|
38
|
+
function createPathDiv(p){
|
39
|
+
const idx = document.querySelectorAll('.path-item').length;
|
40
|
+
const div = document.createElement('div');
|
41
|
+
div.className = 'path-item';
|
42
|
+
div.innerHTML = `
|
43
|
+
<label>Dir: <input type="text" class="pdir" id="dir_${idx}" value="${p?.dir||''}">
|
44
|
+
<input type="file" webkitdirectory directory style="display:none" id="dirsel_${idx}">
|
45
|
+
<button type="button" onclick="document.getElementById('dirsel_${idx}').click()">Select Folder</button></label>
|
46
|
+
<label>Name: <input type="text" class="pname" value="${p?.name||''}"></label>
|
47
|
+
<label>Reader: <select class="preader"></select></label>
|
48
|
+
<label>Threshold: <input type="text" class="pthreshold" value="${p?.threshold||0.3}"></label>
|
49
|
+
<label>Out: <input type="text" class="pout" value="${p?.out||''}"></label>
|
50
|
+
<label>NameMatch: <input type="text" class="pnamematch" value="${p?.nameMatch||''}"></label>
|
51
|
+
<label>URL: <input type="text" class="purl" value="${p?.url||''}"></label>
|
52
|
+
<label>Search Default: <input type="checkbox" class="psearchdefault" ${p?.searchDefault?'checked':''}></label>
|
53
|
+
<button type="button" onclick="this.parentNode.remove()">Remove</button>
|
54
|
+
`;
|
55
|
+
const dirInput = div.querySelector('#dir_'+idx);
|
56
|
+
const nameInput = div.querySelector('.pname');
|
57
|
+
const outInput = div.querySelector('.pout');
|
58
|
+
|
59
|
+
function updateNameOut(){
|
60
|
+
if(!dirInput.value) return;
|
61
|
+
const parts = dirInput.value.replace(/\\/g,'/').split('/').filter(Boolean);
|
62
|
+
const name = parts[parts.length-1] || '';
|
63
|
+
nameInput.value = name;
|
64
|
+
outInput.value = dirInput.value.replace(/[/\\]$/, '') + '/' + name + '.dt';
|
65
|
+
}
|
66
|
+
|
67
|
+
div.querySelector('#dirsel_'+idx).addEventListener('change', function(){
|
68
|
+
if(this.files.length>0){
|
69
|
+
const rel = this.files[0].webkitRelativePath;
|
70
|
+
const dir = rel.split('/')[0];
|
71
|
+
dirInput.value = dir;
|
72
|
+
updateNameOut();
|
73
|
+
}
|
74
|
+
});
|
75
|
+
dirInput.addEventListener('change', updateNameOut);
|
76
|
+
fillReaderSelect(div.querySelector('.preader'), p?.reader||'text');
|
77
|
+
return div;
|
78
|
+
}
|
79
|
+
|
80
|
+
function addPath(p){
|
81
|
+
document.getElementById('paths').appendChild(createPathDiv(p));
|
82
|
+
}
|
83
|
+
|
84
|
+
function loadConfig(readers){
|
85
|
+
READERS = readers;
|
86
|
+
fetch('/config').then(r=>r.json()).then(cfg=>{
|
87
|
+
if(cfg.chat){
|
88
|
+
document.getElementById('chat_provider').value = cfg.chat.provider||'openai';
|
89
|
+
document.getElementById('chat_url').value = cfg.chat.url||'';
|
90
|
+
document.getElementById('chat_model').value = cfg.chat.model||'gpt-3.5-turbo-16k';
|
91
|
+
}
|
92
|
+
if(cfg.embedding){
|
93
|
+
document.getElementById('emb_provider').value = cfg.embedding.provider||'openai';
|
94
|
+
document.getElementById('emb_url').value = cfg.embedding.url||'';
|
95
|
+
document.getElementById('emb_model').value = cfg.embedding.model||'text-embedding-3-small';
|
96
|
+
}
|
97
|
+
if(cfg.paths && cfg.paths.length>0){
|
98
|
+
cfg.paths.forEach(p=>addPath(p));
|
99
|
+
}else{
|
100
|
+
addPath();
|
101
|
+
}
|
102
|
+
});
|
103
|
+
}
|
104
|
+
|
105
|
+
fetch('/readers').then(r=>r.json()).then(loadConfig);
|
106
|
+
|
107
|
+
document.getElementById('config-form').addEventListener('submit', function(e){
|
108
|
+
e.preventDefault();
|
109
|
+
const paths=[];
|
110
|
+
document.querySelectorAll('.path-item').forEach(div=>{
|
111
|
+
paths.push({
|
112
|
+
dir: div.querySelector('.pdir').value,
|
113
|
+
name: div.querySelector('.pname').value,
|
114
|
+
reader: div.querySelector('.preader').value,
|
115
|
+
threshold: parseFloat(div.querySelector('.pthreshold').value)||0,
|
116
|
+
out: div.querySelector('.pout').value,
|
117
|
+
nameMatch: div.querySelector('.pnamematch').value,
|
118
|
+
url: div.querySelector('.purl').value,
|
119
|
+
searchDefault: div.querySelector('.psearchdefault').checked
|
120
|
+
});
|
121
|
+
});
|
122
|
+
const config={
|
123
|
+
chat:{provider:document.getElementById('chat_provider').value,
|
124
|
+
url:document.getElementById('chat_url').value,
|
125
|
+
model:document.getElementById('chat_model').value},
|
126
|
+
embedding:{provider:document.getElementById('emb_provider').value,
|
127
|
+
url:document.getElementById('emb_url').value,
|
128
|
+
model:document.getElementById('emb_model').value},
|
129
|
+
paths:paths
|
130
|
+
};
|
131
|
+
fetch('/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(config)})
|
132
|
+
.then(()=>alert('Saved'));
|
133
|
+
});
|
134
|
+
</script>
|
135
|
+
</body>
|
136
|
+
</html>
|
data/exe/run-index
CHANGED
data/exe/run-server
CHANGED
@@ -9,7 +9,7 @@
|
|
9
9
|
|
10
10
|
require "json"
|
11
11
|
require "ostruct"
|
12
|
-
require
|
12
|
+
require 'sinatra/base'
|
13
13
|
|
14
14
|
require_relative "../server/retriever"
|
15
15
|
require_relative "../server/synthesizer"
|
@@ -23,6 +23,7 @@ end
|
|
23
23
|
config = JSON.parse(File.read(ARGV[0]))
|
24
24
|
CONFIG = OpenStruct.new(config)
|
25
25
|
CONFIG.paths = CONFIG.paths.map { |p| OpenStruct.new(p) }
|
26
|
+
CONFIG.paths.each { |p| p.searchDefault = !!p.searchDefault }
|
26
27
|
CONFIG.path_map = {}
|
27
28
|
CONFIG.paths.each { |p| CONFIG.path_map[p.name] = p }
|
28
29
|
|
@@ -32,33 +33,92 @@ if OPENAI_KEY.empty?
|
|
32
33
|
exit 9
|
33
34
|
end
|
34
35
|
|
35
|
-
|
36
|
-
|
37
|
-
|
36
|
+
class SimpleRagServer < Sinatra::Application
|
37
|
+
# list all the paths that can be searched
|
38
|
+
get '/paths' do
|
39
|
+
content_type :json
|
38
40
|
|
39
|
-
|
40
|
-
|
41
|
-
|
41
|
+
resp = []
|
42
|
+
CONFIG.paths.each do |p|
|
43
|
+
resp << { name: p.name, searchDefault: p.searchDefault }
|
44
|
+
end
|
45
|
+
resp.to_json
|
42
46
|
end
|
43
|
-
resp.to_json
|
44
|
-
end
|
45
47
|
|
46
|
-
# query within the paths
|
47
|
-
post '/q' do
|
48
|
-
|
48
|
+
# query within the paths
|
49
|
+
post '/q' do
|
50
|
+
content_type :json
|
51
|
+
|
52
|
+
data = JSON.parse(request.body.read)
|
49
53
|
|
50
|
-
|
54
|
+
selected = data["paths"]
|
55
|
+
if !selected || selected.empty?
|
56
|
+
selected = CONFIG.paths.select { |p| p.searchDefault }.map(&:name)
|
57
|
+
selected = CONFIG.path_map.keys if selected.empty?
|
58
|
+
end
|
59
|
+
lookup_paths = selected.map { |name| CONFIG.path_map[name] }
|
60
|
+
|
61
|
+
topN = (data["topN"] || 20).to_i
|
62
|
+
|
63
|
+
q = data["q"]
|
64
|
+
entries = retrieve_by_embedding(lookup_paths, q)
|
65
|
+
if q.to_s.strip.length < 5 && q.to_s.split(/\s+/).length < 5
|
66
|
+
entries.concat(retrieve_by_text(lookup_paths, q))
|
67
|
+
|
68
|
+
unique = {}
|
69
|
+
entries.each do |e|
|
70
|
+
key = [e["path"], e["chunk"]]
|
71
|
+
if unique[key]
|
72
|
+
unique[key]["score"] = (unique[key]["score"] || 0) + (e["score"] || 0)
|
73
|
+
else
|
74
|
+
unique[key] = e
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
entries = unique.values
|
79
|
+
end
|
80
|
+
entries = entries.sort_by { |item| -item["score"] }.take(topN)
|
81
|
+
|
82
|
+
resp = {
|
83
|
+
data: [],
|
84
|
+
}
|
51
85
|
|
52
|
-
|
53
|
-
|
86
|
+
entries.each do |item|
|
87
|
+
resp[:data] << {
|
88
|
+
path: item["path"],
|
89
|
+
lookup: item["lookup"],
|
90
|
+
id: item["id"],
|
91
|
+
url: item["url"],
|
92
|
+
text: item["reader"].load.get_chunk(item["chunk"]),
|
93
|
+
score: item["score"],
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
resp.to_json
|
54
98
|
end
|
55
99
|
|
56
|
-
|
100
|
+
# agentic query - expand the query using LLM before searching
|
101
|
+
post '/q_plus' do
|
102
|
+
content_type :json
|
103
|
+
|
104
|
+
data = JSON.parse(request.body.read)
|
105
|
+
|
106
|
+
selected = data["paths"]
|
107
|
+
if !selected || selected.empty?
|
108
|
+
selected = CONFIG.paths.select { |p| p.searchDefault }.map(&:name)
|
109
|
+
selected = CONFIG.path_map.keys if selected.empty?
|
110
|
+
end
|
111
|
+
lookup_paths = selected.map { |name| CONFIG.path_map[name] }
|
112
|
+
|
113
|
+
topN = (data["topN"] || 20).to_i
|
57
114
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
entries
|
115
|
+
expanded_q = expand_query(data["q"])
|
116
|
+
variants = expand_variants(data["q"])
|
117
|
+
|
118
|
+
entries = []
|
119
|
+
entries.concat(retrieve_by_embedding(lookup_paths, data["q"]))
|
120
|
+
entries.concat(retrieve_by_embedding(lookup_paths, expanded_q))
|
121
|
+
variants.each { |v| entries.concat(retrieve_by_text(lookup_paths, v)) }
|
62
122
|
|
63
123
|
unique = {}
|
64
124
|
entries.each do |e|
|
@@ -70,98 +130,49 @@ post '/q' do
|
|
70
130
|
end
|
71
131
|
end
|
72
132
|
|
73
|
-
|
74
|
-
end
|
75
|
-
entries = entries.sort_by { |item| -item["score"] }.take(topN)
|
76
|
-
|
77
|
-
resp = {
|
78
|
-
data: [],
|
79
|
-
}
|
80
|
-
|
81
|
-
entries.each do |item|
|
82
|
-
resp[:data] << {
|
83
|
-
path: item["path"],
|
84
|
-
lookup: item["lookup"],
|
85
|
-
id: item["id"],
|
86
|
-
url: item["url"],
|
87
|
-
text: item["reader"].load.get_chunk(item["chunk"]),
|
88
|
-
score: item["score"],
|
89
|
-
}
|
90
|
-
end
|
91
|
-
|
92
|
-
resp.to_json
|
93
|
-
end
|
133
|
+
ordered = unique.values.sort_by { |item| -item["score"] }.take(topN)
|
94
134
|
|
95
|
-
|
96
|
-
|
97
|
-
|
135
|
+
resp = {
|
136
|
+
data: [],
|
137
|
+
expanded: expanded_q,
|
138
|
+
variants: variants,
|
139
|
+
}
|
98
140
|
|
99
|
-
|
141
|
+
ordered.each do |item|
|
142
|
+
resp[:data] << {
|
143
|
+
path: item["path"],
|
144
|
+
lookup: item["lookup"],
|
145
|
+
id: item["id"],
|
146
|
+
url: item["url"],
|
147
|
+
text: item["reader"].load.get_chunk(item["chunk"]),
|
148
|
+
score: item["score"],
|
149
|
+
}
|
150
|
+
end
|
100
151
|
|
101
|
-
|
102
|
-
CONFIG.path_map[name]
|
152
|
+
resp.to_json
|
103
153
|
end
|
104
154
|
|
105
|
-
|
155
|
+
# synthesize notes into a summary
|
156
|
+
post '/synthesize' do
|
157
|
+
content_type :json
|
106
158
|
|
107
|
-
|
108
|
-
variants = expand_variants(data["q"])
|
159
|
+
data = JSON.parse(request.body.read)
|
109
160
|
|
110
|
-
|
111
|
-
entries.concat(retrieve_by_embedding(lookup_paths, data["q"]))
|
112
|
-
entries.concat(retrieve_by_embedding(lookup_paths, expanded_q))
|
113
|
-
variants.each { |v| entries.concat(retrieve_by_text(lookup_paths, v)) }
|
114
|
-
|
115
|
-
unique = {}
|
116
|
-
entries.each do |e|
|
117
|
-
key = [e["path"], e["chunk"]]
|
118
|
-
if unique[key]
|
119
|
-
unique[key]["score"] = (unique[key]["score"] || 0) + (e["score"] || 0)
|
120
|
-
else
|
121
|
-
unique[key] = e
|
122
|
-
end
|
123
|
-
end
|
161
|
+
summary = synthesize_notes(data["notes"])
|
124
162
|
|
125
|
-
|
126
|
-
|
127
|
-
resp = {
|
128
|
-
data: [],
|
129
|
-
expanded: expanded_q,
|
130
|
-
variants: variants,
|
131
|
-
}
|
132
|
-
|
133
|
-
ordered.each do |item|
|
134
|
-
resp[:data] << {
|
135
|
-
path: item["path"],
|
136
|
-
lookup: item["lookup"],
|
137
|
-
id: item["id"],
|
138
|
-
url: item["url"],
|
139
|
-
text: item["reader"].load.get_chunk(item["chunk"]),
|
140
|
-
score: item["score"],
|
141
|
-
}
|
163
|
+
{ note: summary }.to_json
|
142
164
|
end
|
143
165
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
# synthesize notes into a summary
|
148
|
-
post '/synthesize' do
|
149
|
-
content_type :json
|
166
|
+
# generate discussion for a single note
|
167
|
+
post '/discuss' do
|
168
|
+
content_type :json
|
150
169
|
|
151
|
-
|
170
|
+
data = JSON.parse(request.body.read)
|
152
171
|
|
153
|
-
|
172
|
+
discussion = discuss_note(data["note"])
|
154
173
|
|
155
|
-
|
174
|
+
{ discussion: discussion }.to_json
|
175
|
+
end
|
156
176
|
end
|
157
177
|
|
158
|
-
|
159
|
-
post '/discuss' do
|
160
|
-
content_type :json
|
161
|
-
|
162
|
-
data = JSON.parse(request.body.read)
|
163
|
-
|
164
|
-
discussion = discuss_note(data["note"])
|
165
|
-
|
166
|
-
{ discussion: discussion }.to_json
|
167
|
-
end
|
178
|
+
SimpleRagServer.run!
|
data/exe/run-setup
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
|
4
|
+
# Setup a config JSON interactively via a local web page
|
5
|
+
#
|
6
|
+
# Usage: run-setup config.json
|
7
|
+
|
8
|
+
require "json"
|
9
|
+
require 'sinatra/base'
|
10
|
+
require_relative '../readers/reader'
|
11
|
+
|
12
|
+
if ARGV.length != 1
|
13
|
+
STDOUT << "Invalid arguments received, need a config file\n"
|
14
|
+
exit 1
|
15
|
+
end
|
16
|
+
|
17
|
+
config_path = File.expand_path(ARGV[0])
|
18
|
+
|
19
|
+
class SetupServer < Sinatra::Base
|
20
|
+
set :bind, '0.0.0.0'
|
21
|
+
set :port, 4568
|
22
|
+
set :public_folder, File.expand_path('public', __dir__)
|
23
|
+
set :config_path, nil
|
24
|
+
|
25
|
+
get '/' do
|
26
|
+
send_file File.join(settings.public_folder, 'setup.html')
|
27
|
+
end
|
28
|
+
|
29
|
+
get '/readers' do
|
30
|
+
content_type :json
|
31
|
+
READERS.to_json
|
32
|
+
end
|
33
|
+
|
34
|
+
get '/config' do
|
35
|
+
content_type :json
|
36
|
+
|
37
|
+
if File.exist?(settings.config_path)
|
38
|
+
File.read(settings.config_path)
|
39
|
+
else
|
40
|
+
{}.to_json
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
post '/save' do
|
45
|
+
content_type :json
|
46
|
+
|
47
|
+
data = JSON.parse(request.body.read)
|
48
|
+
File.write(settings.config_path, JSON.pretty_generate(data))
|
49
|
+
|
50
|
+
{ status: 'ok' }.to_json
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
SetupServer.set :config_path, config_path
|
55
|
+
SetupServer.run!
|
data/lib/simple_rag/version.rb
CHANGED
data/lib/simple_rag.rb
CHANGED
data/llm/embedding.rb
CHANGED
File without changes
|
data/llm/http.rb
CHANGED
File without changes
|
data/llm/llm.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require_relative "openai"
|
2
|
+
require_relative "ollama"
|
3
|
+
|
4
|
+
ROLE_SYSTEM = "system"
|
5
|
+
ROLE_USER = "user"
|
6
|
+
ROLE_ASSISTANT = "assistant"
|
7
|
+
NEXT_ROLE = ->(role) { role != ROLE_USER ? ROLE_USER : ROLE_ASSISTANT }
|
8
|
+
|
9
|
+
# Fetch configuration value with defaults
|
10
|
+
# Supports Hash or OpenStruct configuration objects
|
11
|
+
|
12
|
+
def cfg(section, key, default)
|
13
|
+
return default unless defined?(CONFIG)
|
14
|
+
sec = CONFIG.send(section) if CONFIG.respond_to?(section)
|
15
|
+
return default unless sec
|
16
|
+
|
17
|
+
if sec.is_a?(Hash)
|
18
|
+
sec.fetch(key, default)
|
19
|
+
elsif sec.respond_to?(key)
|
20
|
+
val = sec.send(key)
|
21
|
+
val.nil? ? default : val
|
22
|
+
else
|
23
|
+
default
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Route chat requests based on provider configuration
|
28
|
+
|
29
|
+
def chat(messages, opts = {})
|
30
|
+
provider = cfg(:chat, 'provider', 'openai').downcase
|
31
|
+
case provider
|
32
|
+
when 'ollama'
|
33
|
+
model = cfg(:chat, 'model', 'llama2')
|
34
|
+
url = cfg(:chat, 'url', 'http://localhost:11434/api/chat')
|
35
|
+
ollama_chat(messages, model, url, opts)
|
36
|
+
else
|
37
|
+
model = cfg(:chat, 'model', 'gpt-4.1-mini')
|
38
|
+
url = cfg(:chat, 'url', 'https://api.openai.com/v1/chat/completions')
|
39
|
+
openai_chat(messages, model, url, opts)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Route embedding requests based on provider configuration
|
44
|
+
|
45
|
+
def embedding(txts, opts = {})
|
46
|
+
provider = cfg(:embedding, 'provider', 'openai').downcase
|
47
|
+
case provider
|
48
|
+
when 'ollama'
|
49
|
+
model = cfg(:embedding, 'model', 'nomic-embed-text')
|
50
|
+
url = cfg(:embedding, 'url', 'http://localhost:11434/api/embeddings')
|
51
|
+
ollama_embedding(txts, model, url, opts)
|
52
|
+
else
|
53
|
+
model = cfg(:embedding, 'model', 'text-embedding-3-small')
|
54
|
+
url = cfg(:embedding, 'url', 'https://api.openai.com/v1/embeddings')
|
55
|
+
openai_embedding(txts, model, url, opts)
|
56
|
+
end
|
57
|
+
end
|
data/llm/ollama.rb
CHANGED
@@ -1,13 +1,12 @@
|
|
1
1
|
require_relative "http"
|
2
2
|
|
3
|
-
def
|
3
|
+
def ollama_embedding(txts, model, url, opts = {})
|
4
4
|
data = {
|
5
|
-
"model" =>
|
5
|
+
"model" => model,
|
6
6
|
"prompt" => txts
|
7
7
|
}.merge(opts)
|
8
8
|
|
9
|
-
|
10
|
-
response = http_post(uri, nil, data)
|
9
|
+
response = http_post(url, nil, data)
|
11
10
|
|
12
11
|
if response.code != "200"
|
13
12
|
STDOUT << "Embedding error: #{response}\n"
|
@@ -16,4 +15,25 @@ def embedding_ollama(txts, opts = {})
|
|
16
15
|
|
17
16
|
result = JSON.parse(response.body)
|
18
17
|
result["embedding"]
|
18
|
+
end
|
19
|
+
|
20
|
+
def ollama_chat(messages, model, url, opts = {})
|
21
|
+
data = {
|
22
|
+
"model" => model,
|
23
|
+
"messages" => messages
|
24
|
+
}.merge(opts)
|
25
|
+
|
26
|
+
response = http_post(url, nil, data)
|
27
|
+
|
28
|
+
if response.code != "200"
|
29
|
+
STDOUT << "Chat error: #{response}\n"
|
30
|
+
exit 1
|
31
|
+
end
|
32
|
+
|
33
|
+
result = JSON.parse(response.body)
|
34
|
+
if result.is_a?(Hash) && result["message"]
|
35
|
+
result["message"]["content"]
|
36
|
+
else
|
37
|
+
result["choices"][0]["message"]["content"]
|
38
|
+
end
|
19
39
|
end
|
data/llm/openai.rb
CHANGED
@@ -1,18 +1,12 @@
|
|
1
1
|
require_relative "http"
|
2
2
|
|
3
|
-
|
4
|
-
ROLE_USER = "user"
|
5
|
-
ROLE_ASSISTANT = "assistant"
|
6
|
-
NEXT_ROLE = ->(role) { role != ROLE_USER ? ROLE_USER : ROLE_ASSISTANT }
|
7
|
-
|
8
|
-
def chat(messages, opts = {})
|
3
|
+
def openai_chat(messages, model, url, opts = {})
|
9
4
|
data = {
|
10
|
-
"model" =>
|
5
|
+
"model" => model,
|
11
6
|
"messages" => messages
|
12
7
|
}.merge(opts)
|
13
8
|
|
14
|
-
|
15
|
-
response = http_post(uri, OPENAI_KEY, data)
|
9
|
+
response = http_post(url, OPENAI_KEY, data)
|
16
10
|
|
17
11
|
if response.code != "200"
|
18
12
|
STDOUT << "Chat error: #{response}\n"
|
@@ -25,14 +19,13 @@ def chat(messages, opts = {})
|
|
25
19
|
result["choices"][0]["message"]["content"]
|
26
20
|
end
|
27
21
|
|
28
|
-
def
|
22
|
+
def openai_embedding(txts, model, url, opts = {})
|
29
23
|
data = {
|
30
|
-
"model" =>
|
24
|
+
"model" => model,
|
31
25
|
"input" => txts
|
32
26
|
}.merge(opts)
|
33
27
|
|
34
|
-
|
35
|
-
response = http_post(uri, OPENAI_KEY, data)
|
28
|
+
response = http_post(url, OPENAI_KEY, data)
|
36
29
|
|
37
30
|
if response.code != "200"
|
38
31
|
STDOUT << "Embedding error: #{response.body}\n"
|
@@ -41,4 +34,4 @@ def embedding(txts, opts = {})
|
|
41
34
|
|
42
35
|
result = JSON.parse(response.body)
|
43
36
|
result["data"][0]["embedding"]
|
44
|
-
end
|
37
|
+
end
|
data/readers/check-reader.rb
CHANGED
File without changes
|
data/readers/journal.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
class JournalReader
|
2
|
+
SKIP_HEADINGS = ["\u7CBE\u529B", "\u611F\u6069"]
|
3
|
+
|
4
|
+
attr_accessor :file, :chunks
|
5
|
+
|
6
|
+
def initialize(file)
|
7
|
+
@file = file
|
8
|
+
@loaded = false
|
9
|
+
@chunks = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def load
|
13
|
+
return self if @loaded
|
14
|
+
|
15
|
+
parse_journal
|
16
|
+
|
17
|
+
@loaded = true
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def get_chunk(idx)
|
22
|
+
@chunks[idx || 0]
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def parse_journal
|
28
|
+
started = false
|
29
|
+
heading = nil
|
30
|
+
lines = []
|
31
|
+
|
32
|
+
File.foreach(@file) do |line|
|
33
|
+
line = line.chomp
|
34
|
+
next if line.strip.empty?
|
35
|
+
|
36
|
+
if !started
|
37
|
+
next unless line.start_with?("## ")
|
38
|
+
started = true
|
39
|
+
heading = line[3..].strip
|
40
|
+
lines = [clean_line(line)]
|
41
|
+
next
|
42
|
+
end
|
43
|
+
|
44
|
+
if line.start_with?("## ")
|
45
|
+
push_chunk(heading, lines)
|
46
|
+
heading = line[3..].strip
|
47
|
+
lines = [clean_line(line)]
|
48
|
+
next
|
49
|
+
end
|
50
|
+
|
51
|
+
next if line.lstrip.start_with?("<")
|
52
|
+
|
53
|
+
lines << clean_line(line)
|
54
|
+
end
|
55
|
+
|
56
|
+
push_chunk(heading, lines) if started
|
57
|
+
end
|
58
|
+
|
59
|
+
def push_chunk(heading, lines)
|
60
|
+
return if SKIP_HEADINGS.any? { |k| heading.include?(k) }
|
61
|
+
return if lines.length < 3
|
62
|
+
|
63
|
+
@chunks << lines.join("\n")
|
64
|
+
end
|
65
|
+
|
66
|
+
def clean_line(line)
|
67
|
+
line.gsub(/\[([^\]]+)\]\(([^\)]+)\)/, '\\1')
|
68
|
+
end
|
69
|
+
end
|
data/readers/note.rb
CHANGED
File without changes
|
data/readers/reader.rb
CHANGED
@@ -1,12 +1,21 @@
|
|
1
|
+
READERS = %w[text note journal]
|
2
|
+
|
1
3
|
def get_reader(name)
|
2
|
-
case name.downcase
|
4
|
+
case name.to_s.downcase
|
3
5
|
when "text"
|
4
6
|
require_relative "text"
|
5
|
-
|
7
|
+
TextReader
|
6
8
|
when "note"
|
7
9
|
require_relative "note"
|
8
|
-
|
10
|
+
NoteReader
|
11
|
+
when "journal"
|
12
|
+
require_relative "journal"
|
13
|
+
JournalReader
|
9
14
|
else
|
10
|
-
|
15
|
+
nil
|
11
16
|
end
|
12
|
-
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def available_readers
|
20
|
+
READERS
|
21
|
+
end
|
data/readers/text.rb
CHANGED
@@ -12,13 +12,26 @@ class TextReader
|
|
12
12
|
return self if @loaded
|
13
13
|
|
14
14
|
chunk = ""
|
15
|
+
in_frontmatter = false
|
15
16
|
File.foreach(@file) do |line|
|
16
|
-
|
17
|
+
stripped = line.strip
|
18
|
+
|
19
|
+
if in_frontmatter
|
20
|
+
if stripped == '---' || stripped == '...'
|
21
|
+
in_frontmatter = false
|
22
|
+
end
|
23
|
+
next
|
24
|
+
elsif stripped == '---'
|
25
|
+
in_frontmatter = true
|
26
|
+
next
|
27
|
+
end
|
28
|
+
|
29
|
+
if line.start_with?('- ') && line.include?(':') || line.start_with?(' - [[')
|
17
30
|
next
|
18
|
-
elsif line.start_with?('<')
|
31
|
+
elsif line.start_with?('<')
|
19
32
|
next
|
20
33
|
else
|
21
|
-
chunk << line unless
|
34
|
+
chunk << line unless stripped.empty?
|
22
35
|
end
|
23
36
|
end
|
24
37
|
|
data/server/cache.rb
CHANGED
File without changes
|
data/server/discuss.rb
CHANGED
data/server/retriever.rb
CHANGED
@@ -1,15 +1,13 @@
|
|
1
1
|
require "pathname"
|
2
2
|
|
3
3
|
require_relative "cache"
|
4
|
-
|
5
|
-
require_relative "../llm/openai"
|
4
|
+
require_relative "../llm/llm"
|
6
5
|
require_relative "../llm/embedding"
|
7
|
-
|
8
6
|
require_relative "../readers/reader"
|
9
7
|
|
10
8
|
AGENT_PROMPT = <<~PROMPT
|
11
|
-
|
12
|
-
documents. Return only the expanded query in a single line.
|
9
|
+
Expand the user input to a better search query so it is easier to retrieve related markdown
|
10
|
+
documents using embedding. Return only the expanded query in a single line.
|
13
11
|
PROMPT
|
14
12
|
|
15
13
|
def expand_query(q)
|
@@ -17,7 +15,11 @@ def expand_query(q)
|
|
17
15
|
{ role: ROLE_SYSTEM, content: AGENT_PROMPT },
|
18
16
|
{ role: ROLE_USER, content: q },
|
19
17
|
]
|
20
|
-
|
18
|
+
|
19
|
+
query = chat(msgs).strip
|
20
|
+
STDOUT << "Expand query: #{query}\n"
|
21
|
+
|
22
|
+
query
|
21
23
|
end
|
22
24
|
|
23
25
|
def retrieve_by_embedding(lookup_paths, q)
|
@@ -78,8 +80,8 @@ def extract_url(file_path, url)
|
|
78
80
|
end
|
79
81
|
|
80
82
|
VARIANT_PROMPT = <<~PROMPT
|
81
|
-
|
82
|
-
Return
|
83
|
+
Generate three alternative search keywords based on the user input to retrieve related markdown using exact keyword matches.
|
84
|
+
Return the search keywords in one CSV line.
|
83
85
|
PROMPT
|
84
86
|
|
85
87
|
def expand_variants(q)
|
@@ -87,7 +89,10 @@ def expand_variants(q)
|
|
87
89
|
{ role: ROLE_SYSTEM, content: VARIANT_PROMPT },
|
88
90
|
{ role: ROLE_USER, content: q },
|
89
91
|
]
|
90
|
-
|
92
|
+
|
93
|
+
variants = chat(msgs).split(',')
|
94
|
+
STDOUT << "Expand variants: #{variants}\n"
|
95
|
+
variants
|
91
96
|
end
|
92
97
|
|
93
98
|
def retrieve_by_text(lookup_paths, q)
|
data/server/synthesizer.rb
CHANGED
@@ -2,7 +2,7 @@ SUM_PROMPT = """You are an expert at combining notes.
|
|
2
2
|
Given a collection of notes, synthesize them into a concise new note capturing the key points.
|
3
3
|
"""
|
4
4
|
|
5
|
-
require_relative "../llm/
|
5
|
+
require_relative "../llm/llm"
|
6
6
|
|
7
7
|
# notes: array of strings
|
8
8
|
# Returns summary text
|
data/storage/mem.rb
CHANGED
File without changes
|
metadata
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simple-rag-zc
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Zhuochun
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
11
|
date: 2025-06-07 00:00:00.000000000 Z
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '4.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rackup
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.2'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: puma
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -44,21 +58,26 @@ email:
|
|
44
58
|
executables:
|
45
59
|
- run-index
|
46
60
|
- run-server
|
61
|
+
- run-setup
|
47
62
|
extensions: []
|
48
63
|
extra_rdoc_files: []
|
49
64
|
files:
|
50
65
|
- README.md
|
51
66
|
- example_config.json
|
52
67
|
- exe/public/q.html
|
68
|
+
- exe/public/setup.html
|
53
69
|
- exe/run-index
|
54
70
|
- exe/run-server
|
71
|
+
- exe/run-setup
|
55
72
|
- lib/simple_rag.rb
|
56
73
|
- lib/simple_rag/version.rb
|
57
74
|
- llm/embedding.rb
|
58
75
|
- llm/http.rb
|
76
|
+
- llm/llm.rb
|
59
77
|
- llm/ollama.rb
|
60
78
|
- llm/openai.rb
|
61
79
|
- readers/check-reader.rb
|
80
|
+
- readers/journal.rb
|
62
81
|
- readers/note.rb
|
63
82
|
- readers/reader.rb
|
64
83
|
- readers/text.rb
|
@@ -71,7 +90,7 @@ homepage: https://github.com/zhuochun/simple-rag
|
|
71
90
|
licenses:
|
72
91
|
- MIT
|
73
92
|
metadata: {}
|
74
|
-
post_install_message:
|
93
|
+
post_install_message:
|
75
94
|
rdoc_options: []
|
76
95
|
require_paths:
|
77
96
|
- lib
|
@@ -86,8 +105,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
86
105
|
- !ruby/object:Gem::Version
|
87
106
|
version: '0'
|
88
107
|
requirements: []
|
89
|
-
rubygems_version: 3.
|
90
|
-
signing_key:
|
108
|
+
rubygems_version: 3.4.10
|
109
|
+
signing_key:
|
91
110
|
specification_version: 4
|
92
111
|
summary: RAG on Markdown Files
|
93
112
|
test_files: []
|