refactored llmchat

This commit is contained in:
Luxferre
2025-12-20 22:37:45 +02:00
parent 0d2738390b
commit cc62214e16
2 changed files with 199 additions and 169 deletions
+126
View File
@@ -0,0 +1,126 @@
# deck.llm
import json
# init requests library in a platform-agnostic way
requests = None
try:
from deck import net
print('Waiting for online status...')
status, stext, myip = net.wifi_connect_wait()
if status:
print("We're online, initing requests lib...")
requests = net.init_requests()
except:
import requests
class LLMChat:
"""
Class for chat-like LLM interactions
"""
def __init__(self, config_file='llmcfg.json', message_limit=10):
self.config = self.load_config(config_file)
self.config_file = config_file
self.message_limit = message_limit
self.messages = []
self.setup_provider()
self.reset_context()
def load_config(self, config_file='llmcfg.json'):
"""
Loads config from JSON file with CircuitPython fallback.
"""
try:
with open(config_file, 'r') as f:
return json.load(f)
except Exception as e:
print(f"Error: Could not load {config_file} ({e})")
# Minimal fallback for embedded if file is missing
return {
"system_prompt": "Concise assistant",
"temperature": 0.7,
"active_provider": "local",
"providers": {"pollinations": {"base_url": "https://text.pollinations.ai/openai", "api_key": "", "model": "openai-fast"}}
}
def save_config(self):
"""
Saves config. Note: May fail on CircuitPython if USB is connected.
"""
try:
with open(self.config_file, 'w') as f:
json.dump(self.config, f)
return (True, "Configuration saved.")
except OSError as e:
return (False, f"Save failed: {e}. (Is filesystem read-only?)")
def setup_provider(self):
"""
Sets up headers and provider settings.
"""
active_name = self.config.get('active_provider')
providers = self.config.get('providers', {})
if active_name not in providers:
return (False, f"Provider {active_name} not found.")
self.provider = providers[active_name]
self.headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.provider.get('api_key', '')}"
}
extra = self.provider.get('extra_headers', {})
if extra: self.headers.update(extra)
return (True, "Provider ready")
def reset_context(self):
"""
Clears history and re-adds system prompt.
"""
self.messages = [{"role": "system", "content": self.config.get('system_prompt', '')}]
return (True, "Context cleared")
def get_completion(self, user_input):
"""
Sends request to LLM.
"""
self.messages.append({"role": "user", "content": user_input})
if len(self.messages) > (self.message_limit + 1):
self.messages = [self.messages[0]] + self.messages[-self.message_limit:]
payload = {
"model": self.provider.get('model'),
"messages": self.messages,
"temperature": self.config.get('temperature', 0.7),
"stream": False
}
url = f"{self.provider.get('base_url').rstrip('/')}/chat/completions"
try:
response = requests.post(url, headers=self.headers, json=payload)
data = response.json()
response.close() # Important for CircuitPython memory
bot_content = data['choices'][0]['message']['content']
self.messages.append({"role": "assistant", "content": bot_content})
return (True, bot_content)
except Exception as e:
if len(self.messages) > 0: self.messages.pop()
return (False, f"Error: {e}")
def list_models(self):
"""
Lists available models for the active provider.
"""
url = f"{self.provider.get('base_url').rstrip('/')}/models"
try:
response = requests.get(url, headers=self.headers)
data = response.json()
response.close()
res = []
if 'data' in data:
res = [m.get('id') for m in data['data']]
return True, res
except Exception as e:
return False, f"Error: {e}"
+73 -169
View File
@@ -4,185 +4,89 @@
# keyboard to work correctly # keyboard to work correctly
# Created by Luxferre in 2025, released into public domain # Created by Luxferre in 2025, released into public domain
import sys, os, json from deck.chat import DeckChat
from deck.llm import LLMChat
# init the tdeck_repl input
try:
from tdeck_repl import input
except:
pass
# init requests library in a platform-agnostic way
requests = None
try:
from deck import net
print('Waiting for online status...')
status, stext, myip = net.wifi_connect_wait()
print(status, stext, myip)
if status:
print("We're online, initing requests lib...")
requests = net.init_requests()
except:
import requests
CONFIG_FILE = 'llmcfg.json' CONFIG_FILE = 'llmcfg.json'
class LLMChat: # chat handlers
def __init__(self):
self.config = self.load_config()
self.messages = []
self.setup_provider()
self.reset_context()
def load_config(self): def send_msg(msg, state):
"""Loads config from JSON file with CircuitPython fallback.""" print("Please wait for the answer...")
try: status, ans = state.get_completion(msg)
with open(CONFIG_FILE, 'r') as f: return ans, state
return json.load(f)
except Exception as e:
print(f"Error: Could not load {CONFIG_FILE} ({e})")
# Minimal fallback for embedded if file is missing
return {
"system_prompt": "Concise assistant",
"temperature": 0.7,
"active_provider": "local",
"providers": {"pollinations": {"base_url": "https://text.pollinations.ai/openai", "api_key": "", "model": "openai-fast"}}
}
def save_config(self): def set_sys_prompt(prompt, state):
"""Saves config. Note: May fail on CircuitPython if USB is connected.""" if prompt:
try: state.config['system_prompt'] = prompt
with open(CONFIG_FILE, 'w') as f: state.save_config()
json.dump(self.config, f) state.reset_context()
print("Configuration saved.") return 'System prompt updated', state
except OSError as e: else:
print(f"Save failed: {e}. (Is filesystem read-only?)") return f"System: {state.config['system_prompt']}", state
def setup_provider(self): def set_temp(temp, state):
"""Sets up headers and provider settings.""" if temp:
active_name = self.config.get('active_provider') state.config['temperature'] = float(temp)
providers = self.config.get('providers', {}) state.save_config()
return 'Temperature updated', state
if active_name not in providers: else:
print(f"Provider {active_name} not found.") return f"Temperature: {state.config['temperature']}", state
return
self.provider = providers[active_name]
self.headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.provider.get('api_key', '')}"
}
extra = self.provider.get('extra_headers', {})
if extra: self.headers.update(extra)
def reset_context(self): def set_prov(prov, state):
"""Clears history and re-adds system prompt.""" if prov and prov in state.config.get('providers', {}):
self.messages = [{"role": "system", "content": self.config.get('system_prompt', '')}] state.config['active_provider'] = prov
print("Context cleared.") state.setup_provider()
state.save_config()
state.reset_context()
return 'Provider updated', state
else:
return f"Active provider: {state.config['active_provider']}", state
def paginate_output(self, text, lines_per_page=8): def set_model(model, state):
"""Splits output for small terminals.""" if model:
lines = text.split('\n') state.provider['model'] = model
for i in range(0, len(lines), lines_per_page): state.save_config()
chunk = lines[i:i + lines_per_page] return 'Model updated', state
for line in chunk: print(line) else:
if i + lines_per_page < len(lines): return f"Active model: {state.provider.get('model')}", state
input("-- Press Enter --")
def get_completion(self, user_input): def list_models(stub, state):
"""Sends request to LLM.""" status, mlist = state.list_models()
self.messages.append({"role": "user", "content": user_input}) if status:
resp = f"Model list for {state.config['active_provider']}:"
# Memory safety for embedded: keep only last 10 messages for m in mlist:
if len(self.messages) > 11: resp += f'\n- {m}'
self.messages = [self.messages[0]] + self.messages[-10:] else:
resp = mlist
return resp, state
payload = { def list_providers(stub, state):
"model": self.provider.get('model'), resp = 'Provider list:'
"messages": self.messages, for p in state.config.get('providers', {}):
"temperature": self.config.get('temperature', 0.7) resp += f'\n - {p}'
} return resp, state
url = f"{self.provider.get('base_url').rstrip('/')}/chat/completions" def display_help(stub, state):
print("Please wait for the answer...") resp = "/clear, /system, /temp, /prov, /model, /modellist, /provlist, /exit"
return resp, state
try:
response = requests.post(url, headers=self.headers, json=payload)
data = response.json()
response.close() # Important for CircuitPython memory
bot_content = data['choices'][0]['message']['content']
self.messages.append({"role": "assistant", "content": bot_content})
return bot_content
except Exception as e:
if len(self.messages) > 0: self.messages.pop()
return f"Error: {e}"
def list_models(self): def clear_ctx(stub, state):
"""Lists available models.""" state.reset_context()
url = f"{self.provider.get('base_url').rstrip('/')}/models" return 'Context reset', state
try:
response = requests.get(url, headers=self.headers)
data = response.json()
response.close()
print(f"Models for {self.config['active_provider']}:")
if 'data' in data:
for m in data['data']: print(f" - {m.get('id')}")
except Exception as e:
print(f"Error: {e}")
def run(self): # main initialization
print(f"LLM CLI | {self.config.get('active_provider')} | {self.provider.get('model')}") llm = LLMChat(config_file=CONFIG_FILE, message_limit=20)
chat = DeckChat(start_state=llm, chat_prefix='> ', paginate=8)
while True: chat.command('default', send_msg)
try: chat.command('clear', clear_ctx)
user_input = input("\n>> ").strip() chat.command('system', set_sys_prompt)
if not user_input: continue chat.command('temp', set_temp)
chat.command('prov', set_prov)
if user_input.startswith('/'): chat.command('model', set_model)
parts = user_input.split(maxsplit=1) chat.command('modellist', list_models)
cmd, arg = parts[0].lower(), parts[1] if len(parts) > 1 else None chat.command('provlist', list_providers)
chat.command('help', display_help)
if cmd in ['/exit', '/quit']: break chat.command('exit', lambda m,s: ('Exiting', None))
elif cmd == '/clear': self.reset_context() chat.command('quit', lambda m,s: ('Exiting', None))
elif cmd == '/system': chat.start()
if arg:
self.config['system_prompt'] = arg
self.save_config()
self.reset_context()
else: print(f"System: {self.config['system_prompt']}")
elif cmd == '/temp':
if arg:
self.config['temperature'] = float(arg)
self.save_config()
else: print(f"Temp: {self.config['temperature']}")
elif cmd == '/prov':
if arg and arg in self.config.get('providers', {}):
self.config['active_provider'] = arg
self.setup_provider()
self.save_config()
self.reset_context()
else: print(f"Active: {self.config['active_provider']}")
elif cmd == '/model':
if arg:
self.provider['model'] = arg
self.save_config()
else: print(f"Model: {self.provider.get('model')}")
elif cmd == '/modellist': self.list_models()
elif cmd == '/provlist':
for p in self.config.get('providers', {}): print(f" - {p}")
elif cmd == '/help':
print("/clear, /system, /temp, /prov, /model, /modellist, /provlist, /exit")
else: print("Unknown command.")
continue
ans = self.get_completion(user_input)
print("")
self.paginate_output(ans)
except KeyboardInterrupt: break
print("\nExiting.")
# no main, direct init
chat = LLMChat()
chat.run()