From e66ebd50a943cec712d54fc0d8d024e88f580356 Mon Sep 17 00:00:00 2001 From: Luxferre Date: Fri, 26 Dec 2025 14:10:10 +0200 Subject: [PATCH] Implemented vibecoding functionality for llmchat --- README.md | 10 ++++- app/llmchat.py | 109 ++++++++++++++++++++++++++++++++++++++++++------- deck/llm.py | 34 +++++++++++++++ tdeckboot.py | 1 + 4 files changed, 138 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 8ad764c..bf15de8 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,9 @@ The instance exposes the following methods (all returning a `(status, result)` t - `reset_context()`: clear the conversation history and re-add the system prompt - `get_completion(prompt)`: prompt the LLM and return the response message - `list_models()`: list available model IDs from the current active provider +- `get_last_response()`: get the most recent assistant's response as a plain string +- `add_file(fname)`: read a text file and add it to the context (within the message limit) +- `export()`: export the entire current conversation (within the message limit) as a plain string ## Applets (`app.*`) @@ -241,7 +244,7 @@ The editor mode supports the following subset of POSIX ed commands in the standa ### `app.llmchat` -A simple interactive chat with remote LLMs. Configured via `llmcfg.json` (see the `deck.llm` module reference) placed into the storage root. +A simple interactive chat with remote LLMs. Configured via `llmcfg.json` (see the `deck.llm` module reference) placed into the storage root. Run with the single `llmchat()` method it exports. Supported chat commands: @@ -253,6 +256,11 @@ Supported chat commands: - `/model`: display or set the current model selection - `/provlist`: list provider IDs in the config - `/modellist`: list model IDs available for current provider +- `/add`: add a (text) file to the context +- `/saveconv`: save the entire conversation log (within the current limits) into a file +- `/savelast`: save the most recent message into a file +- `/savecode`: extract code blocks from the most recent message and save them into a file +- `/edcode`: extract code blocks from the most recent message and open `app.ed.edbuf()` on the contents - `/exit` or `/quit`: exit the chat applet ### `app.blog` diff --git a/app/llmchat.py b/app/llmchat.py index 48e037f..1a6108b 100644 --- a/app/llmchat.py +++ b/app/llmchat.py @@ -2,10 +2,12 @@ # (also can be run on MicroPython and usual CPython) # On the T-Deck, requires to be run from tdeck_repl for the # keyboard to work correctly +# Usage: from llmchat import llmchat # Created by Luxferre in 2025, released into public domain from deck.chat import DeckChat from deck.llm import LLMChat +from app.ed import edbuf CONFIG_FILE = 'llmcfg.json' @@ -67,8 +69,79 @@ def list_providers(stub, state): resp += f'\n - {p}' return resp, state +def save_conversation(fname, state): + resp = '' + status, cnv = state.export() + try: + with open(fname, 'w') as f: + f.write(cnv) + resp = 'Conversation saved' + except: + resp = 'Error saving conversation' + return resp, state + +def save_last_msg(fname, state): + resp = '' + status, cnv = state.get_last_response() + try: + with open(fname, 'w') as f: + f.write(cnv) + resp = 'Response saved' + except: + resp = 'Error saving response' + return resp, state + +def extract_code_block(text, start_marker="```", end_marker="```"): + extracted_parts = [] + search_index = 0 + while True: + block_start = text.find(start_marker, search_index) + if block_start == -1: + break + content_start = block_start + len(start_marker) + if start_marker == "```": + newline_after_start = text.find("\n", content_start) + if newline_after_start != -1: + potential_end = text.find(end_marker, content_start) + if potential_end == -1 or newline_after_start < potential_end: + content_start = newline_after_start + 1 + block_end = text.find(end_marker, content_start) + if block_end == -1: + break + extracted_block = text[content_start:block_end].strip() + if extracted_block: + extracted_parts.append(extracted_block) + search_index = block_end + len(end_marker) + return "\n\n".join(extracted_parts) + +def save_last_msg_code(fname, state): + resp = '' + status, cnv = state.get_last_response() + cnv = extract_code_block(cnv) + try: + with open(fname, 'w') as f: + f.write(cnv) + resp = 'Code saved' + except: + resp = 'Error saving code' + return resp, state + +def edit_last_msg_code(fname, state): + resp = '' + status, cnv = state.get_last_response() + cnv = extract_code_block(cnv) + try: + edbuf(cnv) + except: + resp = 'Error extracting code' + return resp, state + +def add_file(fname, state): + status, resp = state.add_file(fname) + return resp, state + def display_help(stub, state): - resp = "/clear, /system, /temp, /prov, /model, /modellist, /provlist, /exit" + resp = "/clear, /system, /temp, /prov, /model, /modellist, /provlist, /add, /saveconv, /savelast, /savecode, /edcode, /exit, /quit" return resp, state def clear_ctx(stub, state): @@ -76,17 +149,23 @@ def clear_ctx(stub, state): return 'Context reset', state # main initialization -llm = LLMChat(config_file=CONFIG_FILE, message_limit=20) -chat = DeckChat(start_state=llm, chat_prefix='> ') -chat.command('default', send_msg) -chat.command('clear', clear_ctx) -chat.command('system', set_sys_prompt) -chat.command('temp', set_temp) -chat.command('prov', set_prov) -chat.command('model', set_model) -chat.command('modellist', list_models) -chat.command('provlist', list_providers) -chat.command('help', display_help) -chat.command('exit', lambda m,s: ('Exiting...', None)) -chat.command('quit', lambda m,s: ('Exiting...', None)) -chat.start() +def llmchat(): + llm = LLMChat(config_file=CONFIG_FILE, message_limit=20) + chat = DeckChat(start_state=llm, chat_prefix='> ') + chat.command('default', send_msg) + chat.command('clear', clear_ctx) + chat.command('system', set_sys_prompt) + chat.command('temp', set_temp) + chat.command('prov', set_prov) + chat.command('model', set_model) + chat.command('modellist', list_models) + chat.command('provlist', list_providers) + chat.command('add', add_file) + chat.command('saveconv', save_conversation) + chat.command('savelast', save_last_msg) + chat.command('savecode', save_last_msg_code) + chat.command('edcode', edit_last_msg_code) + chat.command('help', display_help) + chat.command('exit', lambda m,s: ('Exiting...', None)) + chat.command('quit', lambda m,s: ('Exiting...', None)) + chat.start() diff --git a/deck/llm.py b/deck/llm.py index 84df7ba..c9cfc55 100644 --- a/deck/llm.py +++ b/deck/llm.py @@ -111,6 +111,40 @@ class LLMChat: if len(self.messages) > 0: self.messages.pop() return (False, f"Error: {e}") + def get_last_response(self): + """ + Get the most recent assistant's response. + """ + idx = len(self.messages) - 1 + while idx > 0 and self.messages[idx]['role'] != 'assistant': + idx -= 1 + if idx == 0: + return (False, 'No assistant messages detected') + else: + return (True, self.messages[idx]['content']) + + def export(self): + """ + Export the entire conversation as a string. + """ + out = '' + for msg in self.messages: + out += f'{msg['role']}: {msg['content'].strip()}\n' + return (True, out.strip()) + + def add_file(self, fname): + """ + Add a (text) file to the context. + """ + try: + with open(fname, 'r') as f: + text = f.read() + prompt = f'Contents of the file {fname}:\n\n{text}' + self.messages.append({'role': 'user', 'content': prompt}) + return (True, 'File content added') + except: + return (False, 'Could not read the file') + def list_models(self): """ Lists available models for the active provider. diff --git a/tdeckboot.py b/tdeckboot.py index e7a29ac..47fd112 100644 --- a/tdeckboot.py +++ b/tdeckboot.py @@ -14,6 +14,7 @@ from deck.http import auth_header, url_escape, form_encode from deck import xlat from app.ed import edit, view, edbuf, viewbuf, edurl, viewurl from app.blog import blogpost, blogpost_str, bloged +from app.llmchat import llmchat try: from deck import net requests = net.init_requests()