vimlm 0.0.2__py3-none-any.whl → 0.0.3__py3-none-any.whl
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.
- {vimlm-0.0.2.dist-info → vimlm-0.0.3.dist-info}/METADATA +1 -1
- vimlm-0.0.3.dist-info/RECORD +7 -0
- vimlm-0.0.3.dist-info/top_level.txt +1 -0
- vimlm.py +371 -0
- vimlm-0.0.2.dist-info/RECORD +0 -6
- vimlm-0.0.2.dist-info/top_level.txt +0 -1
- {vimlm-0.0.2.dist-info → vimlm-0.0.3.dist-info}/LICENSE +0 -0
- {vimlm-0.0.2.dist-info → vimlm-0.0.3.dist-info}/WHEEL +0 -0
- {vimlm-0.0.2.dist-info → vimlm-0.0.3.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,7 @@
|
|
1
|
+
vimlm.py,sha256=ku7NOIrcCNHzRYP9_8qp1NX8w5uCiZAlf0ijMKJXTlw,12996
|
2
|
+
vimlm-0.0.3.dist-info/LICENSE,sha256=f1xgK8fAXg_intwnbc9nLkHf7ODPLtgpHs7DetQHOro,11343
|
3
|
+
vimlm-0.0.3.dist-info/METADATA,sha256=i4W8tmpaMH1m86vzEwNpwo9BUfYcLzr3nksEU_iGW6Y,3178
|
4
|
+
vimlm-0.0.3.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
5
|
+
vimlm-0.0.3.dist-info/entry_points.txt,sha256=mU5V4MYsuIzCc6YB-Ro-6USSHWN5vHw8UDnTEoq0isw,36
|
6
|
+
vimlm-0.0.3.dist-info/top_level.txt,sha256=I8GjqoiP--scYsO3AfLhha-6Ax9ci3IvbWvVbPv8g94,6
|
7
|
+
vimlm-0.0.3.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
vimlm
|
vimlm.py
ADDED
@@ -0,0 +1,371 @@
|
|
1
|
+
# Copyright 2025 Josef Albers
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
import asyncio
|
16
|
+
import subprocess
|
17
|
+
import json
|
18
|
+
import os
|
19
|
+
from watchfiles import awatch
|
20
|
+
from nanollama32 import Chat
|
21
|
+
import shutil
|
22
|
+
import time
|
23
|
+
from itertools import accumulate
|
24
|
+
import argparse
|
25
|
+
import tempfile
|
26
|
+
from pathlib import Path
|
27
|
+
from string import Template
|
28
|
+
|
29
|
+
DEBUG = False
|
30
|
+
NUM_TOKEN = 2000
|
31
|
+
SEP_CMD = '!@#$'
|
32
|
+
CACHE_DIR = os.path.expanduser("~/vimlm")
|
33
|
+
WATCH_DIR = os.path.expanduser("~/vimlm/watch_dir")
|
34
|
+
LOG_FILE = "log.json"
|
35
|
+
CACHE_FILE = "cache.json"
|
36
|
+
OUT_FILE = "response.md"
|
37
|
+
FILES = ["context", "yank", "user", "tree"]
|
38
|
+
LOG_PATH = os.path.join(CACHE_DIR, LOG_FILE)
|
39
|
+
CACHE_PATH = os.path.join(CACHE_DIR, CACHE_FILE)
|
40
|
+
OUT_PATH = os.path.join(WATCH_DIR, OUT_FILE)
|
41
|
+
|
42
|
+
if os.path.exists(WATCH_DIR):
|
43
|
+
shutil.rmtree(WATCH_DIR)
|
44
|
+
os.makedirs(WATCH_DIR)
|
45
|
+
|
46
|
+
def toout(s, key='tovim'):
|
47
|
+
with open(OUT_PATH, 'w', encoding='utf-8') as f:
|
48
|
+
f.write(s)
|
49
|
+
tolog(s, key)
|
50
|
+
|
51
|
+
def tolog(log, key='debug'):
|
52
|
+
if not DEBUG and key == 'debug':
|
53
|
+
return
|
54
|
+
if os.path.exists(LOG_PATH):
|
55
|
+
with open(LOG_PATH, "r", encoding="utf-8") as log_f:
|
56
|
+
logs = json.load(log_f)
|
57
|
+
else:
|
58
|
+
logs = []
|
59
|
+
logs.append(dict(key=key, log=log, timestamp=time.ctime()))
|
60
|
+
with open(LOG_PATH, "w", encoding="utf-8") as log_f:
|
61
|
+
json.dump(logs, log_f, indent=2)
|
62
|
+
|
63
|
+
toout('Loading LLM...')
|
64
|
+
chat = Chat(variant='uncn_llama_32_3b_it')
|
65
|
+
toout('LLM is ready')
|
66
|
+
|
67
|
+
def is_binary(file_path):
|
68
|
+
try:
|
69
|
+
with open(file_path, 'rb') as f:
|
70
|
+
chunk = f.read(1024)
|
71
|
+
chunk.decode('utf-8')
|
72
|
+
return False
|
73
|
+
except UnicodeDecodeError:
|
74
|
+
return True
|
75
|
+
except Exception as e:
|
76
|
+
return f"Error: {e}"
|
77
|
+
|
78
|
+
def split_str(doc, max_len=2000, get_len=len):
|
79
|
+
chunks, current_chunk, current_len = [], [], 0
|
80
|
+
lines = doc.splitlines(keepends=True)
|
81
|
+
atomic_chunks, temp = [], []
|
82
|
+
for line in lines:
|
83
|
+
if line.strip():
|
84
|
+
temp.append(line)
|
85
|
+
else:
|
86
|
+
if temp:
|
87
|
+
atomic_chunks.append("".join(temp))
|
88
|
+
temp = []
|
89
|
+
atomic_chunks.append(line)
|
90
|
+
if temp:
|
91
|
+
atomic_chunks.append("".join(temp))
|
92
|
+
for chunk in atomic_chunks:
|
93
|
+
if current_len + get_len(chunk) > max_len and current_chunk:
|
94
|
+
chunks.append("".join(current_chunk))
|
95
|
+
current_chunk, current_len = [], 0
|
96
|
+
current_chunk.append(chunk)
|
97
|
+
current_len += get_len(chunk)
|
98
|
+
if current_chunk:
|
99
|
+
if current_len < max_len / 2 and len(chunks) > 0:
|
100
|
+
chunks[-1] += "".join(current_chunk)
|
101
|
+
else:
|
102
|
+
chunks.append("".join(current_chunk))
|
103
|
+
return chunks
|
104
|
+
|
105
|
+
def get_context(src_path, max_len=2000, get_len=len):
|
106
|
+
src_path = os.path.expanduser(src_path)
|
107
|
+
result = {}
|
108
|
+
if not os.path.exists(src_path):
|
109
|
+
tolog(f"The path {src_path} does not exist.", 'get_context')
|
110
|
+
return result
|
111
|
+
if os.path.isfile(src_path):
|
112
|
+
try:
|
113
|
+
with open(src_path, 'r', encoding='utf-8', errors='ignore') as f:
|
114
|
+
content = f.read()
|
115
|
+
result = {src_path:dict(timestamp=os.path.getmtime(src_path), list_str=split_str(content, max_len=max_len, get_len=get_len))}
|
116
|
+
except Exception as e:
|
117
|
+
tolog(f'Skipped {filename} due to {e}', 'get_context')
|
118
|
+
return result
|
119
|
+
for filename in os.listdir(src_path):
|
120
|
+
try:
|
121
|
+
file_path = os.path.join(src_path, filename)
|
122
|
+
if filename.startswith('.') or is_binary(file_path):
|
123
|
+
continue
|
124
|
+
if os.path.isfile(file_path):
|
125
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
126
|
+
content = f.read()
|
127
|
+
result[file_path] = dict(timestamp=os.path.getmtime(file_path), list_str=split_str(content, max_len=max_len, get_len=get_len))
|
128
|
+
except Exception as e:
|
129
|
+
tolog(f'Skipped {filename} due to {e}', 'get_context')
|
130
|
+
continue
|
131
|
+
return result
|
132
|
+
|
133
|
+
def get_ntok(s):
|
134
|
+
return len(chat.tokenizer.encode(s)[0])
|
135
|
+
|
136
|
+
def ingest(src):
|
137
|
+
def load_cache(cache_path=CACHE_PATH):
|
138
|
+
if os.path.exists(cache_path):
|
139
|
+
with open(cache_path, 'r', encoding='utf-8') as f:
|
140
|
+
return json.load(f)
|
141
|
+
return {}
|
142
|
+
def dump_cache(new_data, cache_path=CACHE_PATH):
|
143
|
+
current_data = load_cache(cache_path)
|
144
|
+
for k, v in new_data.items():
|
145
|
+
if k not in current_data or v['timestamp'] > current_data[k]['timestamp']:
|
146
|
+
current_data[k] = v
|
147
|
+
with open(cache_path, 'w', encoding='utf-8') as f:
|
148
|
+
json.dump(current_data, f, indent=2)
|
149
|
+
toout('Ingesting...')
|
150
|
+
format_ingest = '{volat}{incoming}\n\n---\n\nPlease provide a succint bullet point summary for above:'
|
151
|
+
format_volat = 'Here is a summary of part 1 of **{k}**:\n\n---\n\n{newsum}\n\n---\n\nHere is the next part:\n\n---\n\n'
|
152
|
+
dict_doc = get_context(src, get_len=get_ntok)
|
153
|
+
dict_sum = {}
|
154
|
+
cache = load_cache()
|
155
|
+
for k, v in dict_doc.items():
|
156
|
+
v_stamp = v['timestamp']
|
157
|
+
if v_stamp == cache.get(k, {}).get('timestamp'):
|
158
|
+
dict_sum[k] = cache[k]
|
159
|
+
continue
|
160
|
+
list_str = v['list_str']
|
161
|
+
if len(list_str) == 0:
|
162
|
+
continue
|
163
|
+
if len(list_str) > 1:
|
164
|
+
max_new_sum = int(NUM_TOKEN / len(list_str) / 2)
|
165
|
+
volat = f'**{k}**:\n'
|
166
|
+
accum = ''
|
167
|
+
for s in list_str:
|
168
|
+
chat.reset()
|
169
|
+
newsum = chat(format_ingest.format(volat=volat, incoming=s.strip()), max_new=max_new_sum, verbose=False, stream=None)[0][:-10].strip()
|
170
|
+
accum += newsum + ' ...\n'
|
171
|
+
volat = format_volat.format(k=k, newsum=newsum)
|
172
|
+
else:
|
173
|
+
accum = list_str[0]
|
174
|
+
chat.reset()
|
175
|
+
toout('')
|
176
|
+
chat_summary = chat(format_ingest.format(volat=f'**{k}**:\n', incoming=accum), max_new=int(NUM_TOKEN/4), verbose=False, stream=OUT_PATH)[0][:-10].strip()
|
177
|
+
dict_sum[k] = dict(timestamp=v_stamp, summary=chat_summary)
|
178
|
+
dump_cache(dict_sum)
|
179
|
+
result = ''
|
180
|
+
for (k,v) in dict_sum.items():
|
181
|
+
result += f'--- Summary of **{k}** ---\n{v["summary"]}\n\n'
|
182
|
+
result += '---\n\n'
|
183
|
+
return result
|
184
|
+
|
185
|
+
def process_command(data, str_template):
|
186
|
+
try:
|
187
|
+
if len(data['user_command'].strip()) > 0:
|
188
|
+
src = data['user_command']
|
189
|
+
else:
|
190
|
+
src = data['dir']
|
191
|
+
return ingest(src) + str_template
|
192
|
+
except Exception as e:
|
193
|
+
tolog(f'Failed to ingest due to {e}', 'process_command')
|
194
|
+
return str_template
|
195
|
+
|
196
|
+
async def monitor_directory():
|
197
|
+
async for changes in awatch(WATCH_DIR):
|
198
|
+
found_files = {os.path.basename(f) for _, f in changes}
|
199
|
+
tolog(f'{found_files=}') # DEBUG
|
200
|
+
if FILES[-1] in found_files and set(FILES).issubset(set(os.listdir(WATCH_DIR))):
|
201
|
+
tolog(f'listdir()={os.listdir(WATCH_DIR)}') # DEBUG
|
202
|
+
data = {}
|
203
|
+
for file in FILES:
|
204
|
+
path = os.path.join(WATCH_DIR, file)
|
205
|
+
with open(path, 'r', encoding='utf-8') as f:
|
206
|
+
data[file] = f.read().strip()
|
207
|
+
os.remove(os.path.join(WATCH_DIR, file))
|
208
|
+
if 'followup' in os.listdir(WATCH_DIR):
|
209
|
+
os.remove(os.path.join(WATCH_DIR, 'followup'))
|
210
|
+
data['followup'] = True
|
211
|
+
str_template = '{user}'
|
212
|
+
await process_files(data)
|
213
|
+
|
214
|
+
async def process_files(data):
|
215
|
+
tolog(f'{data=}')
|
216
|
+
if 'followup' in data:
|
217
|
+
str_template = "{user}"
|
218
|
+
else:
|
219
|
+
full_path = data['tree']
|
220
|
+
data['dir'] = os.path.dirname(full_path)
|
221
|
+
data['file'] = os.path.basename(full_path)
|
222
|
+
data['ext'] = os.path.splitext(full_path)[1][1:]
|
223
|
+
str_template = ''
|
224
|
+
if len(data['file']) > 0 and data['file'] != '.tmp':
|
225
|
+
str_template += '**{file}**\n'
|
226
|
+
if len(data['context']) > 0:
|
227
|
+
str_template += '```{ext}\n{context}\n```\n\n'
|
228
|
+
if len(data['yank']) > 0:
|
229
|
+
if '\n' in data['yank']:
|
230
|
+
str_template += "```{ext}\n{yank}\n```\n\n"
|
231
|
+
else:
|
232
|
+
str_template += "`{yank}` "
|
233
|
+
str_template += '{user_prompt}'
|
234
|
+
if SEP_CMD in data['user']:
|
235
|
+
data['user_prompt'], data['user_command'] = (x.strip() for x in data['user'].split(SEP_CMD))
|
236
|
+
str_template = process_command(data, str_template)
|
237
|
+
else:
|
238
|
+
data['user_prompt'] = data['user']
|
239
|
+
chat.reset()
|
240
|
+
tolog(f'{str_template=}') # DEBUG
|
241
|
+
prompt = str_template.format(**data)
|
242
|
+
tolog(prompt, 'tollm')
|
243
|
+
toout('')
|
244
|
+
response = chat(prompt, max_new=NUM_TOKEN - get_ntok(prompt), verbose=False, stream=OUT_PATH)[0][:-10].strip()
|
245
|
+
toout(response)
|
246
|
+
|
247
|
+
VIMLMSCRIPT = Template(r"""
|
248
|
+
let s:watched_dir = expand('$WATCH_DIR')
|
249
|
+
|
250
|
+
function! Monitor()
|
251
|
+
write
|
252
|
+
let response_path = s:watched_dir . '/response.md'
|
253
|
+
rightbelow vsplit | execute 'view ' . response_path
|
254
|
+
setlocal autoread
|
255
|
+
setlocal readonly
|
256
|
+
setlocal nobuflisted
|
257
|
+
filetype detect
|
258
|
+
syntax on
|
259
|
+
wincmd h
|
260
|
+
let s:monitor_timer = timer_start(100, 'CheckForUpdates', {'repeat': -1})
|
261
|
+
endfunction
|
262
|
+
|
263
|
+
function! CheckForUpdates(timer)
|
264
|
+
let bufnum = bufnr(s:watched_dir . '/response.md')
|
265
|
+
if bufnum == -1
|
266
|
+
call timer_stop(s:monitor_timer)
|
267
|
+
return
|
268
|
+
endif
|
269
|
+
silent! checktime
|
270
|
+
endfunction
|
271
|
+
|
272
|
+
function! s:CustomInput(prompt) abort
|
273
|
+
let input = ''
|
274
|
+
let clean_prompt = a:prompt
|
275
|
+
let aborted = v:false
|
276
|
+
echohl Question
|
277
|
+
echon clean_prompt
|
278
|
+
echohl None
|
279
|
+
while 1
|
280
|
+
let c = getchar()
|
281
|
+
if type(c) == v:t_number
|
282
|
+
if c == 13 " Enter
|
283
|
+
break
|
284
|
+
elseif c == 27 " Esc
|
285
|
+
let aborted = v:true
|
286
|
+
break
|
287
|
+
elseif c == 8 || c == 127 " Backspace
|
288
|
+
if len(input) > 0
|
289
|
+
let input = input[:-2]
|
290
|
+
echon "\r" . clean_prompt . input . ' '
|
291
|
+
execute "normal! \<BS>"
|
292
|
+
endif
|
293
|
+
else
|
294
|
+
let char = nr2char(c)
|
295
|
+
let input .= char
|
296
|
+
echon char
|
297
|
+
endif
|
298
|
+
endif
|
299
|
+
endwhile
|
300
|
+
let input_length = aborted ? 0 : len(input)
|
301
|
+
let clear_length = len(clean_prompt) + input_length
|
302
|
+
echon "\r" . repeat(' ', clear_length) . "\r"
|
303
|
+
return aborted ? v:null : input
|
304
|
+
endfunction
|
305
|
+
|
306
|
+
function! SaveUserInput()
|
307
|
+
let user_input = s:CustomInput('Ask LLM: ')
|
308
|
+
if user_input is v:null
|
309
|
+
echo "Input aborted"
|
310
|
+
return
|
311
|
+
endif
|
312
|
+
let user_file = s:watched_dir . '/user'
|
313
|
+
call writefile([user_input], user_file, 'w')
|
314
|
+
let current_file = expand('%:p')
|
315
|
+
let tree_file = s:watched_dir . '/tree'
|
316
|
+
call writefile([current_file], tree_file, 'w')
|
317
|
+
endfunction
|
318
|
+
|
319
|
+
function! SaveUserInput()
|
320
|
+
let user_input = s:CustomInput('Ask LLM: ')
|
321
|
+
if user_input is v:null
|
322
|
+
echo "Input aborted"
|
323
|
+
return
|
324
|
+
endif
|
325
|
+
let user_file = s:watched_dir . '/user'
|
326
|
+
call writefile([user_input], user_file, 'w')
|
327
|
+
let current_file = expand('%:p')
|
328
|
+
let tree_file = s:watched_dir . '/tree'
|
329
|
+
call writefile([current_file], tree_file, 'w')
|
330
|
+
endfunction
|
331
|
+
|
332
|
+
function! FollowUpInput()
|
333
|
+
call writefile([], s:watched_dir . '/yank', 'w')
|
334
|
+
call writefile([], s:watched_dir . '/context', 'w')
|
335
|
+
call writefile([], s:watched_dir . '/followup', 'w')
|
336
|
+
call SaveUserInput()
|
337
|
+
endfunction
|
338
|
+
|
339
|
+
vnoremap <C-l> :w! $WATCH_DIR/yank<CR>:w! $WATCH_DIR/context<CR>:call SaveUserInput()<CR>
|
340
|
+
nnoremap <C-l> V:w! $WATCH_DIR/yank<CR>:w! $WATCH_DIR/context<CR>:call SaveUserInput()<CR>
|
341
|
+
nnoremap <C-r> :call FollowUpInput()<CR>
|
342
|
+
|
343
|
+
call Monitor()
|
344
|
+
""").safe_substitute(dict(WATCH_DIR=WATCH_DIR))
|
345
|
+
|
346
|
+
async def main():
|
347
|
+
parser = argparse.ArgumentParser(description="VimLM - LLM-powered Vim assistant")
|
348
|
+
parser.add_argument("vim_args", nargs=argparse.REMAINDER, help="Vim arguments")
|
349
|
+
args = parser.parse_args()
|
350
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.vim', delete=False) as f:
|
351
|
+
f.write(VIMLMSCRIPT)
|
352
|
+
vim_script = f.name
|
353
|
+
vim_command = ["vim", "-c", f"source {vim_script}"]
|
354
|
+
if args.vim_args:
|
355
|
+
vim_command.extend(args.vim_args)
|
356
|
+
else:
|
357
|
+
vim_command.append('.tmp')
|
358
|
+
monitor_task = asyncio.create_task(monitor_directory())
|
359
|
+
vim_process = await asyncio.create_subprocess_exec(*vim_command)
|
360
|
+
await vim_process.wait()
|
361
|
+
monitor_task.cancel()
|
362
|
+
try:
|
363
|
+
await monitor_task
|
364
|
+
except asyncio.CancelledError:
|
365
|
+
pass
|
366
|
+
|
367
|
+
def run():
|
368
|
+
asyncio.run(main())
|
369
|
+
|
370
|
+
if __name__ == '__main__':
|
371
|
+
run()
|
vimlm-0.0.2.dist-info/RECORD
DELETED
@@ -1,6 +0,0 @@
|
|
1
|
-
vimlm-0.0.2.dist-info/LICENSE,sha256=f1xgK8fAXg_intwnbc9nLkHf7ODPLtgpHs7DetQHOro,11343
|
2
|
-
vimlm-0.0.2.dist-info/METADATA,sha256=ntusBBeCTMoPlEFM7HBrHzCU1scKWQ09Dx6QA_Rod0s,3178
|
3
|
-
vimlm-0.0.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
4
|
-
vimlm-0.0.2.dist-info/entry_points.txt,sha256=mU5V4MYsuIzCc6YB-Ro-6USSHWN5vHw8UDnTEoq0isw,36
|
5
|
-
vimlm-0.0.2.dist-info/top_level.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
6
|
-
vimlm-0.0.2.dist-info/RECORD,,
|
@@ -1 +0,0 @@
|
|
1
|
-
|
File without changes
|
File without changes
|
File without changes
|