Úplně nová generace.

This commit is contained in:
sinuhet 2025-03-20 00:42:48 +01:00
parent 41a392c0de
commit 3affdd3b0f

View File

@ -1,9 +1,31 @@
#!/usr/bin/env python3
"""
Robovojtík Linuxový shell asistent s interaktivním rozhraním
Funkce:
Rozhraní rozdělené vertikálně na dvě části:
- Levý panel: chat (dotazy, odpovědi, potvrzení, historie)
- Pravý panel: výpis příkazů a jejich výstup.
Podpora automatického režimu (automód), kdy se příkazy spouštějí bez potvrzení.
Při vykonání příkazu se jeho výstup zobrazí v pravém panelu a odešle se asistenti jako report,
přičemž do reportu se přidá indikace úspěšnosti (pokud nedošlo k chybě).
Spinner se zobrazuje ve vyhrazeném okně pokaždé, když čekáme na odpověď z API (dotazy nebo reporty).
V historii chatu se zaznamenává i potvrzení uživatele.
Pokud je zapnuto logování (--log), loguje se vše na úrovni DEBUG asynchronně do souboru "robovojtik.log".
"""
import openai import openai
import subprocess import subprocess
import time import curses
import configparser import configparser
import json import json
import sys import sys
import logging
import threading
import time
import argparse
import queue
import logging.handlers
# === 1. Načtení konfigurace z config.ini === # === 1. Načtení konfigurace z config.ini ===
config = configparser.ConfigParser() config = configparser.ConfigParser()
@ -11,9 +33,19 @@ config.read("config.ini")
openai.api_key = config["OpenAI"]["api_key"] openai.api_key = config["OpenAI"]["api_key"]
ASSISTANT_ID = config["OpenAI"]["assistant_id"] ASSISTANT_ID = config["OpenAI"]["assistant_id"]
thread_id = None
# === 2. Definice funkcí, které asistent může volat === # Globální proměnné
automode = False # Automatický režim: pokud True, příkaz se spustí ihned bez potvrzení.
log_enabled = False # Zapnutí logování nastaví se příkazovou volbou.
thread_id = None # ID vlákna konverzace s OpenAI
# Asynchroní logování
logger = logging.getLogger("robovojtik")
logger.setLevel(logging.DEBUG)
log_queue = None
listener = None
# === 2. Definice funkcí pro volání OpenAI API ===
FUNCTIONS = [ FUNCTIONS = [
{ {
"name": "execute_shell_command", "name": "execute_shell_command",
@ -26,64 +58,121 @@ FUNCTIONS = [
"description": "Shellový příkaz k vykonání." "description": "Shellový příkaz k vykonání."
} }
}, },
"additionalProperties": False, "required": ["command"],
"required": ["command"] "additionalProperties": False
}
},
{
"name": "get_system_load",
"description": "Získá aktuální zátěž systému (load average).",
"parameters": {
"type": "object",
"properties": {},
"required": [],
"additionalProperties": False
}
},
{
"name": "create_script",
"description": "Vytvoří skript s daným obsahem v souboru a nastaví ho na spustitelný.",
"parameters": {
"type": "object",
"properties": {
"file_name": {
"type": "string",
"description": "Název souboru (nebo cesta), do kterého se skript uloží."
},
"content": {
"type": "string",
"description": "Obsah skriptu, který se má uložit."
}
},
"required": ["file_name", "content"],
"additionalProperties": False
} }
} }
] ]
# === 3. Pomocné funkce === # === 3. (System prompt není načítán ze zdrojového kódu, vše se spravuje externě) ===
def vytvor_nove_vlakno():
"""Vytvoří nové vlákno pro zachování historie konverzace."""
thread = openai.beta.threads.create()
return thread.id
def spust_prikaz(prikaz): # === 4. Pomocné funkce pro komunikaci s OpenAI API ===
"""Spustí shellový příkaz a vrátí jeho výstup."""
try: def vytvor_nove_vlakno():
output = subprocess.check_output(prikaz, shell=True, stderr=subprocess.STDOUT, universal_newlines=True) """Vytvoří nové vlákno konverzace s asistentem a vrátí jeho ID."""
return output.strip() global thread_id
except subprocess.CalledProcessError as e: thread = openai.beta.threads.create()
return f"Chyba při vykonávání příkazu:\n{e.output}" thread_id = thread.id
logger.debug(f"Vytvořeno nové vlákno: {thread_id}")
return thread_id
def clean_command(command):
"""Odstraní zpětné apostrofy a nežádoucí znaky z příkazu."""
return command.replace("`", "").strip()
def volani_asistenta(prompt, spinner_func=None):
"""
Spustí posli_dotaz_do_assistenta(prompt) v samostatném vlákně a během čekání volá spinner_func.
Vrací odpověď od asistenta.
"""
result_container = {}
def worker():
odpoved = posli_dotaz_do_assistenta(prompt)
result_container['answer'] = odpoved
thread = threading.Thread(target=worker)
thread.start()
idx = 0
spinner = ["|", "/", "-", "\\"]
while thread.is_alive():
if spinner_func:
spinner_func(spinner[idx % len(spinner)])
time.sleep(0.2)
idx += 1
thread.join()
return result_container.get('answer', "")
def posli_dotaz_do_assistenta(prompt): def posli_dotaz_do_assistenta(prompt):
"""Odesílá dotaz v přirozeném jazyce do asistenta a vrací jeho odpověď.""" """Odesílá dotaz v přirozeném jazyce do asistenta a vrací jeho odpověď."""
global thread_id global thread_id
if thread_id is None: if thread_id is None:
thread_id = vytvor_nove_vlakno() vytvor_nove_vlakno()
logger.debug(f"Odesílám dotaz: {prompt}")
openai.beta.threads.messages.create( openai.beta.threads.messages.create(
thread_id=thread_id, thread_id=thread_id,
role="user", role="user",
content=prompt content=prompt
) )
run = openai.beta.threads.runs.create( run = openai.beta.threads.runs.create(
thread_id=thread_id, thread_id=thread_id,
assistant_id=ASSISTANT_ID assistant_id=ASSISTANT_ID
) )
while True: while True:
run_status = openai.beta.threads.runs.retrieve(thread_id=thread_id, run_id=run.id) run_status = openai.beta.threads.runs.retrieve(thread_id=thread_id, run_id=run.id)
if run_status.status == "completed": if run_status.status == "completed":
break break
time.sleep(1) time.sleep(1)
messages = openai.beta.threads.messages.list(thread_id=thread_id) messages = openai.beta.threads.messages.list(thread_id=thread_id)
return messages.data[0].content[0].text.value answer = messages.data[0].content[0].text.value
logger.debug(f"Asistent odpověděl: {answer}")
return answer
def posli_prikaz_do_assistenta(command): def posli_prikaz_do_assistenta(command):
"""Odesílá schválený příkaz k vykonání asistentovi a vrací výstup příkazu.""" """
Odesílá schválený příkaz k vykonání asistentovi a vrací jeho výstup.
Příkaz se před odesláním vyčistí.
"""
command = clean_command(command)
global thread_id global thread_id
if thread_id is None: if thread_id is None:
thread_id = vytvor_nove_vlakno() vytvor_nove_vlakno()
logger.debug(f"Odesílám příkaz k vykonání: {command}")
run = openai.beta.threads.runs.create( run = openai.beta.threads.runs.create(
thread_id=thread_id, thread_id=thread_id,
assistant_id=ASSISTANT_ID, assistant_id=ASSISTANT_ID,
instructions=f"Prosím, spusť tento příkaz: {command}" instructions=f"Prosím, spusť tento příkaz: {command}"
) )
while True: while True:
run_status = openai.beta.threads.runs.retrieve(thread_id=thread_id, run_id=run.id) run_status = openai.beta.threads.runs.retrieve(thread_id=thread_id, run_id=run.id)
if run_status.status == "requires_action": if run_status.status == "requires_action":
@ -94,6 +183,8 @@ def posli_prikaz_do_assistenta(command):
arguments = json.loads(tool_call.function.arguments) arguments = json.loads(tool_call.function.arguments)
if tool_name == "execute_shell_command": if tool_name == "execute_shell_command":
vysledek = spust_prikaz(arguments["command"]) vysledek = spust_prikaz(arguments["command"])
elif tool_name == "create_script":
vysledek = vytvor_skript(arguments["file_name"], arguments["content"])
else: else:
vysledek = "Neznámá funkce." vysledek = "Neznámá funkce."
tool_outputs.append({ tool_outputs.append({
@ -108,9 +199,10 @@ def posli_prikaz_do_assistenta(command):
elif run_status.status == "completed": elif run_status.status == "completed":
break break
time.sleep(1) time.sleep(1)
messages = openai.beta.threads.messages.list(thread_id=thread_id) messages = openai.beta.threads.messages.list(thread_id=thread_id)
return messages.data[0].content[0].text.value answer = messages.data[0].content[0].text.value
logger.debug(f"Výsledek spuštěného příkazu: {answer}")
return answer
def is_command_response(response): def is_command_response(response):
"""Vrací True, pokud odpověď začíná prefixem indikujícím návrh příkazu.""" """Vrací True, pokud odpověď začíná prefixem indikujícím návrh příkazu."""
@ -123,78 +215,205 @@ def extract_command(response):
return response.strip()[len(prefix):].strip() return response.strip()[len(prefix):].strip()
return response.strip() return response.strip()
def provide_help(): # === 5. Funkce pro spouštění příkazů a report ===
"""Vrací text s nápovědou o funkcích Robovojtíka."""
help_text = (
"Nápověda k Robovojtikovi Linuxový Shell Asistent:\n\n"
"Funkce:\n"
" • Převádí dotazy v přirozeném jazyce na návrhy shellových příkazů.\n"
" • Před spuštěním příkazu vždy čeká na potvrzení od uživatele.\n"
" • Přímé příkazy zadejte s prefixem 'cmd:' (např. 'cmd: ls -la').\n"
" • Pro ukončení Robovojtíka zadejte 'vypni' nebo 'exit'.\n\n"
"Pokud potřebujete další pomoc, zeptejte se!"
)
return help_text
# === 4. Hlavní interaktivní smyčka === def spust_prikaz(command):
def main(): """
print("Linuxový shell s OpenAI Assistant v2 API a funkcemi.") Spustí příkaz synchronně a vrátí jeho výstup.
print("Pro přímý příkaz použij prefix 'cmd:'") Používá se při volání funkce execute_shell_command.
print("Pro ukončení Robovojtíka zadej 'vypni' nebo 'exit'.") """
print("Pro nápovědu zadej 'nápověda' nebo 'help'.") try:
logger.debug(f"Lokálně spouštím příkaz: {command}")
output = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, universal_newlines=True)
return output.strip()
except subprocess.CalledProcessError as e:
return f"Chyba při vykonávání příkazu:\n{e.output}"
def vytvor_skript(nazev, obsah):
"""
Zapíše obsah do souboru s daným názvem a nastaví spustitelnost.
"""
try:
with open(nazev, "w") as f:
f.write(obsah)
subprocess.call(f"chmod +x {nazev}", shell=True)
logger.debug(f"Skript {nazev} vytvořen a nastaven jako spustitelný.")
return f"Skript {nazev} byl úspěšně vytvořen."
except Exception as e:
return f"Nastala chyba při tvorbě skriptu: {str(e)}"
def run_command_locally_and_report(command):
"""
Spustí příkaz lokálně, vyčistí ho a odešle jeho výstup jako report.
Pokud příkaz proběhne bez chyby, reportu přidá informaci, že příkaz proběhl úspěšně.
Vrací tuple (output, assistant_response).
"""
command = clean_command(command)
output = spust_prikaz(command)
if not output.startswith("Chyba při vykonávání příkazu:"):
report_text = f"Výstup příkazu (příkaz proběhl úspěšně):\n{output}"
else:
report_text = f"Výstup příkazu:\n{output}"
answer = posli_dotaz_do_assistenta(report_text)
return output, answer
# === 6. Interaktivní rozhraní s curses (vertikální rozdělení) ===
def main_curses(stdscr):
curses.curs_set(1)
stdscr.nodelay(False)
stdscr.clear()
height, width = stdscr.getmaxyx()
# Rozdělíme levý panel (chat) a pravý panel (výstup).
left_width = width // 2
right_width = width - left_width
# Vytvoříme chat_win s rezervovanou oblastí pro spinner.
chat_win = curses.newwin(height - 2, left_width, 0, 0)
spinner_win = curses.newwin(1, left_width, height - 2, 0)
prompt_win = curses.newwin(1, width, height - 1, 0)
output_win = curses.newwin(height - 1, right_width, 0, left_width)
chat_win.scrollok(True)
output_win.scrollok(True)
output_win.box()
output_win.refresh()
spinner_chars = ["|", "/", "-", "\\"]
def spinner_func(ch):
spinner_win.erase()
spinner_win.addstr(0, 0, f"Čekám na Robovojtíka... {ch}")
spinner_win.refresh()
# Úvodní text v chatu
chat_win.addstr("Vítejte v Robovojtikovi!\n")
chat_win.addstr("Napište dotaz, příkaz ('cmd: ls'), 'automat' pro přepnutí automódu,\n")
chat_win.addstr("'skript: nazev; obsah' pro vytvoření skriptu, nebo 'vypni'/'exit' pro ukončení.\n")
chat_win.refresh()
while True: while True:
user_input = input("\nZadej příkaz v přirozeném jazyce: ").strip() prompt_win.erase()
prompt_win.addstr(">> ")
prompt_win.refresh()
curses.echo()
try:
user_input = prompt_win.getstr().decode("utf-8").strip()
except:
continue
curses.noecho()
# Ukončení Robovojtíka if not user_input:
if user_input.lower() in ["vypni", "exit"]: continue
potvrzeni = input("Skutečně chceš ukončit Robovojtíka? (y/n): ").strip().lower()
if potvrzeni == "y": if user_input.lower() in ["vypni", "exit"]:
print("Ukončuji Robovojtíka...") chat_win.addstr("Ukončuji Robovojtíka...\n")
sys.exit(0) chat_win.refresh()
else: time.sleep(1)
print("Ukončení zrušeno. Pokračuji.") break
continue
if user_input.lower() == "automat":
# Zobrazení nápovědy global automode
if user_input.lower() in ["nápověda", "help"]: automode = not automode
print("\n" + provide_help()) stav = "zapnut" if automode else "vypnut"
chat_win.addstr(f"Automód byl nyní {stav}.\n")
chat_win.refresh()
continue
if user_input.lower().startswith("skript:"):
try:
_, rest = user_input.split("skript:", 1)
nazev, obsah = rest.split(";", 1)
nazev = nazev.strip()
obsah = obsah.strip()
vysledek = vytvor_skript(nazev, obsah)
chat_win.addstr(vysledek + "\n")
except Exception as e:
chat_win.addstr(f"Chyba při vytváření skriptu: {str(e)}\n")
chat_win.refresh()
continue continue
# Přímý příkaz bez asistenta
if user_input.startswith("cmd:"): if user_input.startswith("cmd:"):
command = user_input[4:].strip() command = user_input[4:].strip()
print(f"Rozpoznán přímý příkaz: {command}") chat_win.addstr(f"Rozpoznán přímý příkaz: {command}\n")
potvrzeni = input("Spustit tento příkaz? (y/n): ").strip().lower() chat_win.refresh()
if potvrzeni == "y": if not automode:
vysledek = spust_prikaz(command) prompt_win.erase()
print("\nVýstup příkazu:") prompt_win.addstr("Spustit tento příkaz? (y/n): ")
print(vysledek) prompt_win.refresh()
else: potvrzeni = prompt_win.getstr().decode("utf-8").strip().lower()
print("Příkaz nebyl spuštěn.") # Zaznamenáme potvrzení do chatu
chat_win.addstr(f"Ty: {potvrzeni}\n")
chat_win.refresh()
if potvrzeni != "y":
chat_win.addstr("Příkaz nebyl spuštěn.\n")
chat_win.refresh()
continue
output, response = run_command_locally_and_report(command)
output_win.erase()
output_win.addstr(1, 1, output)
output_win.box()
output_win.refresh()
chat_win.addstr("Robovojtík odpovídá:\n" + response + "\n")
chat_win.refresh()
continue continue
# Dotaz pro asistenta očekává návrh příkazu # Pokud se jedná o dotaz v přirozeném jazyce
assistant_response = posli_dotaz_do_assistenta(user_input) chat_win.addstr(f"Vy: {user_input}\n")
chat_win.refresh()
assistant_response = volani_asistenta(user_input, spinner_func=spinner_func)
spinner_win.erase()
spinner_win.refresh()
chat_win.addstr("Robovojtík odpovídá:\n" + assistant_response + "\n")
chat_win.refresh()
# Pokud odpověď nevypadá jako návrh příkazu, zobrazíme ji jako informativní odpověď if is_command_response(assistant_response):
if not is_command_response(assistant_response): navrhovany_prikaz = extract_command(assistant_response)
print("\nRobovojtík odpovídá:") chat_win.addstr(f"Navrhovaný příkaz: {navrhovany_prikaz}\n")
print(assistant_response) chat_win.refresh()
continue if not automode:
prompt_win.erase()
prompt_win.addstr("Spustit tento příkaz? (y/n): ")
prompt_win.refresh()
potvrzeni = prompt_win.getstr().decode("utf-8").strip().lower()
chat_win.addstr(f"Ty: {potvrzeni}\n")
chat_win.refresh()
if potvrzeni != "y":
chat_win.addstr("Příkaz nebyl spuštěn.\n")
chat_win.refresh()
continue
output, response = run_command_locally_and_report(navrhovany_prikaz)
output_win.erase()
output_win.addstr(1, 1, output)
output_win.box()
output_win.refresh()
chat_win.addstr("Robovojtík odpovídá:\n" + response + "\n")
chat_win.refresh()
# Pokud odpověď obsahuje navržený příkaz, vyzveme uživatele k potvrzení def main():
print("\nRobovojtík navrhuje příkaz:") global log_enabled, log_handler, log_queue, listener
print(assistant_response) parser = argparse.ArgumentParser(description="Robovojtík interaktivní shell asistent")
parser.add_argument("--log", action="store_true", help="Zapne logování do souboru robovojtik.log")
args = parser.parse_args()
potvrzeni = input("Spustit tento příkaz? (y/n): ").strip().lower() if args.log:
if potvrzeni == "y": log_enabled = True
command_to_execute = extract_command(assistant_response) log_queue = queue.Queue(-1)
execution_result = posli_prikaz_do_assistenta(command_to_execute) qh = logging.handlers.QueueHandler(log_queue)
print("\nVýstup příkazu:") logger.addHandler(qh)
print(execution_result) logger.setLevel(logging.DEBUG)
else: fh = logging.FileHandler("robovojtik.log")
print("Příkaz nebyl spuštěn. Čekám na další vstup.") fh.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
listener = logging.handlers.QueueListener(log_queue, fh)
listener.start()
logger.debug("Logování zapnuto.")
curses.wrapper(main_curses)
if listener:
listener.stop()
if __name__ == "__main__": if __name__ == "__main__":
main( main()