Compare commits

...

14 Commits

Author SHA1 Message Date
28c24d5057 Aktualizovat system_prompt 2025-03-22 23:25:23 +01:00
2e2a18272a Oprava, po které to ale blbne
Po příkazu se ptá stále dokola
2025-03-22 23:24:23 +01:00
49d93096d4 Přidat functions/read_file.json 2025-03-20 02:42:07 +01:00
3f527022e6 Přidat functions/create_script.json 2025-03-20 02:41:37 +01:00
e0f597f21a Přidat functions/get_system_load.json 2025-03-20 02:41:02 +01:00
1e1890b7e9 revert 054db0753db91e2c7b99d72802026bb4790fca78
revert Přidat funcitons/get_system_load.json
2025-03-20 02:40:29 +01:00
054db0753d Přidat funcitons/get_system_load.json 2025-03-20 02:39:51 +01:00
9c5e7ae3b4 Přidání funkcí z OpenAI API 2025-03-20 02:39:10 +01:00
2d76d82572 Vylepšený system prompt. 2025-03-20 02:33:09 +01:00
31cd4ddfd3 Základní system_prompt 2025-03-20 02:32:01 +01:00
a83fba1a23 Zřejmě to stále funguje 2025-03-20 02:01:54 +01:00
4e3d3466f1 Další vylepšení, zatím to zjevně nějak funguje. Základní podpora pro Markdown. 2025-03-20 01:41:23 +01:00
d619fc0f3d začátek nové verze 2025-03-20 01:32:29 +01:00
9003d676ac Odstranit robovojtik.py 2025-03-20 01:31:27 +01:00
12 changed files with 897 additions and 412 deletions

197
api_interface.py Normal file
View File

@ -0,0 +1,197 @@
"""
Module: api_interface.py
--------------------------
Obsahuje funkce pro komunikaci s OpenAI API a správu konverzačního vlákna.
"""
import openai
import json
import time
import threading
import logging
from configparser import ConfigParser
logger = logging.getLogger("robovojtik.api_interface")
config = ConfigParser()
config.read("config.ini")
try:
openai.api_key = config["OpenAI"]["api_key"]
except KeyError:
raise ValueError("API key not found in config.ini. Zadejte 'api_key' do [OpenAI].")
ASSISTANT_ID = config["OpenAI"].get("assistant_id", None)
if not ASSISTANT_ID:
raise ValueError("assistant_id not found in config.ini. Zadejte 'assistant_id' do [OpenAI].")
# Globální vlákno konverzace
thread_id = None
def vytvor_nove_vlakno():
"""
Vytvoří nové vlákno konverzace s asistentem pomocí OpenAI API.
"""
global thread_id
try:
thread = openai.beta.threads.create()
thread_id = thread.id
logger.debug(f"Vytvořeno nové vlákno: {thread_id}")
return thread_id
except Exception as e:
logger.exception("Chyba při vytváření nového vlákna konverzace.")
raise
def posli_dotaz_do_assistenta(prompt):
"""
Odesílá dotaz v přirozeném jazyce do asistenta a vrací jeho odpověď.
Ošetřuje stav "queued", "running", "pending" i "in_progress" tak, aby cyklus čekal na dokončení.
Vrací poslední zprávu od asistenta.
"""
global thread_id
if thread_id is None:
vytvor_nove_vlakno()
logger.debug(f"Odesílám dotaz: {prompt}")
openai.beta.threads.messages.create(
thread_id=thread_id,
role="user",
content=prompt
)
run = openai.beta.threads.runs.create(
thread_id=thread_id,
assistant_id=ASSISTANT_ID
)
while True:
run_status = openai.beta.threads.runs.retrieve(thread_id=thread_id, run_id=run.id)
if run_status.status == "requires_action":
handle_required_action(run_status, run.id, thread_id)
elif run_status.status == "completed":
break
elif run_status.status in ["queued", "running", "pending", "in_progress"]:
time.sleep(1)
else:
logger.warning(f"Neočekávaný stav run_status: {run_status.status}")
time.sleep(1)
break
messages = openai.beta.threads.messages.list(thread_id=thread_id)
answer = ""
for msg in reversed(messages.data):
if msg.role == "assistant" and msg.content and msg.content[0].text:
answer = msg.content[0].text.value
break
logger.debug(f"Asistent odpověděl: {answer}")
return answer
def handle_required_action(run_status, run_id, thread_id):
"""
Vyřídí volání funkcí, které asistent vyžaduje (execute_shell_command, create_script, read_file).
"""
from shell_functions import spust_prikaz, vytvor_skript, read_file_content
try:
tool_calls = run_status.required_action.submit_tool_outputs.tool_calls
tool_outputs = []
for tool_call in tool_calls:
tool_name = tool_call.function.name
logger.debug(f"Asistent volá funkci: {tool_name}")
try:
arguments = json.loads(tool_call.function.arguments)
except Exception as e:
logger.exception("Chyba při parsování argumentů pro funkci.")
tool_outputs.append({
"tool_call_id": tool_call.id,
"output": "Chyba: Nepodařilo se přečíst argumenty."
})
continue
if tool_name == "execute_shell_command":
vysledek = spust_prikaz(arguments["command"])
elif tool_name == "create_script":
vysledek = vytvor_skript(arguments["file_name"], arguments["content"])
elif tool_name == "read_file":
vysledek = read_file_content(arguments["path"])
else:
vysledek = "Neznámá funkce."
tool_outputs.append({
"tool_call_id": tool_call.id,
"output": vysledek
})
openai.beta.threads.runs.submit_tool_outputs(
thread_id=thread_id,
run_id=run_id,
tool_outputs=tool_outputs
)
except Exception as e:
logger.exception("Chyba při handle_required_action.")
def posli_prikaz_do_assistenta(command):
"""
Odesílá schválený příkaz k vykonání do asistenta (např. cmd:).
Ošetřuje stavy podobně jako posli_dotaz_do_assistenta.
"""
global thread_id
if thread_id is None:
vytvor_nove_vlakno()
logger.debug(f"Odesílám příkaz k vykonání: {command}")
run = openai.beta.threads.runs.create(
thread_id=thread_id,
assistant_id=ASSISTANT_ID,
instructions=f"Prosím, spusť tento příkaz: {command}"
)
while True:
run_status = openai.beta.threads.runs.retrieve(thread_id=thread_id, run_id=run.id)
if run_status.status == "requires_action":
handle_required_action(run_status, run.id, thread_id)
elif run_status.status == "completed":
break
elif run_status.status in ["queued", "running", "pending", "in_progress"]:
time.sleep(1)
else:
logger.warning(f"Neočekávaný stav run_status: {run_status.status}")
time.sleep(1)
break
messages = openai.beta.threads.messages.list(thread_id=thread_id)
answer = ""
for msg in reversed(messages.data):
if msg.role == "assistant" and msg.content and msg.content[0].text:
answer = msg.content[0].text.value
break
logger.debug(f"Výsledek spuštěného příkazu: {answer}")
return answer
def volani_asistenta(prompt, spinner_func=None):
"""
Spustí dotaz do asistenta v samostatném vlákně a během čekání volá spinner_func.
Vrací odpověď asistenta nebo chybovou hlášku.
"""
result_container = {}
def worker():
try:
odpoved = posli_dotaz_do_assistenta(prompt)
result_container['answer'] = odpoved
except Exception as ex:
result_container['answer'] = f"Chyba při volání asistenta: {str(ex)}"
logger.exception("Chyba v worker funkci volani_asistenta.")
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 reset_thread():
"""
Resetuje konverzační vlákno tím, že nastaví globální thread_id na None.
"""
global thread_id
thread_id = None

View File

@ -0,0 +1,27 @@
{
"name": "create_script",
"description": "Vytvoří skript na základě popisu činnosti. Pokud je zadán popis, asistent vygeneruje obsah skriptu odpovídající tomuto popisu; pokud je zadán i přímý obsah, použije se ten. Skript bude uložen do zadaného souboru a nastaven jako spustitelný.",
"strict": false,
"parameters": {
"type": "object",
"properties": {
"file_name": {
"type": "string",
"description": "Název souboru (nebo cesta), do kterého se skript uloží."
},
"description": {
"type": "string",
"description": "Popis činnosti, kterou má skript vykonávat. Asistent na základě tohoto popisu vygeneruje obsah skriptu."
},
"content": {
"type": "string",
"description": "Nepovinný parametr. Pokud je explicitně zadán, bude použit jako obsah skriptu místo generovaného textu."
}
},
"required": [
"file_name",
"description"
],
"additionalProperties": false
}
}

View File

@ -0,0 +1,18 @@
{
"name": "execute_shell_command",
"description": "Spustí shellový příkaz a vrátí jeho výstup.",
"strict": true,
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Shellový příkaz k vykonání."
}
},
"additionalProperties": false,
"required": [
"command"
]
}
}

View File

@ -0,0 +1,11 @@
{
"name": "get_system_load",
"description": "Získá aktuální zátěž systému (load average).",
"strict": true,
"parameters": {
"type": "object",
"properties": {},
"additionalProperties": false,
"required": []
}
}

18
functions/read_file.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "read_file",
"description": "Načte soubor o maximální velikosti 10kB a vrátí jeho obsah (případně zkrácený).",
"strict": true,
"parameters": {
"type": "object",
"required": [
"path"
],
"properties": {
"path": {
"type": "string",
"description": "Cesta k souboru, který se má přečíst."
}
},
"additionalProperties": false
}
}

99
main.py Normal file
View File

@ -0,0 +1,99 @@
"""
Module: main.py
---------------
Hlavní spouštěcí skript pro Robovojtíka.
Importuje moduly pro API komunikaci, shellové funkce a uživatelské rozhraní.
Zde také definujeme seznam OpenAI funkcí (FUNCTIONS) včetně read_file.
"""
import argparse
import queue
import logging
import logging.handlers
import ui
# Definice OpenAI funkcí, které asistent může volat
FUNCTIONS = [
{
"name": "execute_shell_command",
"description": "Spustí shellový příkaz a vrátí jeho výstup.",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Shellový příkaz k vykonání."
}
},
"required": ["command"],
"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
}
},
{
"name": "read_file",
"description": "Načte soubor o maximální velikosti 10kB a vrátí jeho obsah (případně zkrácený).",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Cesta k souboru, který se má přečíst."
}
},
"required": ["path"],
"additionalProperties": False
}
}
]
def main():
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()
listener = None
if args.log:
import logging.handlers
log_queue = queue.Queue(-1)
logger = logging.getLogger("robovojtik")
logger.setLevel(logging.DEBUG)
qh = logging.handlers.QueueHandler(log_queue)
logger.addHandler(qh)
fh = logging.FileHandler("robovojtik.log", mode="a", encoding="utf-8")
fh.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s"))
listener = logging.handlers.QueueListener(log_queue, fh)
listener.start()
logger.debug("Logování zapnuto.")
# Předáme seznam FUNCTIONS do UI
ui.set_functions(FUNCTIONS)
ui.main_ui()
if listener:
listener.stop()
if __name__ == "__main__":
main()

77
markdown_parser.py Normal file
View File

@ -0,0 +1,77 @@
"""
Module: markdown_parser.py
----------------------------
Rozšířený Markdown parser pro curses.
...
"""
import re
import curses
def parse_markdown(text):
segments = []
lines = text.splitlines()
in_code_block = False
for line in lines:
stripped = line.strip()
if stripped.startswith("```"):
in_code_block = not in_code_block
continue
if in_code_block:
# celé řádky ve vícerádkovém kódu
segments.append((line + "\n", curses.A_REVERSE))
continue
# Nadpisy
if stripped.startswith("#"):
hash_count = 0
for ch in line:
if ch == '#':
hash_count += 1
else:
break
content = line[hash_count:].strip()
segments.append((content + "\n", curses.A_BOLD | curses.A_UNDERLINE))
# Odrážky
elif stripped.startswith("-") or stripped.startswith("*"):
content = line[1:].strip()
segments.append(("" + content + "\n", curses.A_BOLD))
else:
# Běžný řádek
inline_segments = parse_inline(line)
for seg_text, seg_attr in inline_segments:
segments.append((seg_text, seg_attr))
segments.append(("\n", 0))
return segments
def parse_inline(text):
segments = []
pos = 0
pattern = re.compile(r'(\*\*.*?\*\*|\*.*?\*|`.*?`)')
for match in pattern.finditer(text):
start, end = match.span()
if start > pos:
segments.append((text[pos:start], 0))
token = match.group(0)
if token.startswith("**") and token.endswith("**"):
content = token[2:-2]
segments.append((content, curses.A_BOLD))
elif token.startswith("*") and token.endswith("*"):
content = token[1:-1]
segments.append((content, curses.A_UNDERLINE))
elif token.startswith("`") and token.endswith("`"):
content = token[1:-1]
segments.append((content, curses.A_REVERSE))
else:
segments.append((token, 0))
pos = end
if pos < len(text):
segments.append((text[pos:], 0))
return segments

View File

@ -1,411 +0,0 @@
#!/usr/bin/env python3
"""
Robovojtík Linuxový shell asistent s vylepšeným interaktivním rozhraním
Funkce:
Rozhraní s barevným oddělením:
- Levá strana je rozdělena na hlavičku (s trvalými instrukcemi a spinnerem) a chat (historie).
- Pravá strana zobrazuje výstup příkazů.
- Mezi levým a pravým panelem je vertikální oddělovač.
- Dolní část je vstupní oblast (o dva řádky).
Spinner se zobrazuje v hlavičce, uprostřed, vždy když čekáme na odpověď asistenta.
Do chatu se zaznamenává i uživatelské potvrzení.
Logování (při spuštění s --log) běží asynchronně a loguje vše na úrovni DEBUG do souboru "robovojtik.log".
"""
import openai
import subprocess
import curses
import configparser
import json
import sys
import logging
import threading
import time
import argparse
import queue
import logging.handlers
# === 1. Načtení konfigurace z config.ini ===
config = configparser.ConfigParser()
config.read("config.ini")
openai.api_key = config["OpenAI"]["api_key"]
ASSISTANT_ID = config["OpenAI"]["assistant_id"]
# Globální proměnné
automode = False # Automatický režim
log_enabled = False # Zapnutí logování přes --log
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 = [
{
"name": "execute_shell_command",
"description": "Spustí shellový příkaz a vrátí jeho výstup.",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Shellový příkaz k vykonání."
}
},
"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. (System prompt není načítán ze zdrojového kódu) ===
# === 4. Pomocné funkce pro komunikaci s OpenAI API ===
def vytvor_nove_vlakno():
global thread_id
thread = openai.beta.threads.create()
thread_id = thread.id
logger.debug(f"Vytvořeno nové vlákno: {thread_id}")
return thread_id
def clean_command(command):
return command.replace("`", "").strip()
def volani_asistenta(prompt, spinner_func=None):
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):
global thread_id
if thread_id is None:
vytvor_nove_vlakno()
logger.debug(f"Odesílám dotaz: {prompt}")
openai.beta.threads.messages.create(
thread_id=thread_id,
role="user",
content=prompt
)
run = openai.beta.threads.runs.create(
thread_id=thread_id,
assistant_id=ASSISTANT_ID
)
while True:
run_status = openai.beta.threads.runs.retrieve(thread_id=thread_id, run_id=run.id)
if run_status.status == "completed":
break
time.sleep(1)
messages = openai.beta.threads.messages.list(thread_id=thread_id)
answer = messages.data[0].content[0].text.value
logger.debug(f"Asistent odpověděl: {answer}")
return answer
def posli_prikaz_do_assistenta(command):
command = clean_command(command)
global thread_id
if thread_id is None:
vytvor_nove_vlakno()
logger.debug(f"Odesílám příkaz k vykonání: {command}")
run = openai.beta.threads.runs.create(
thread_id=thread_id,
assistant_id=ASSISTANT_ID,
instructions=f"Prosím, spusť tento příkaz: {command}"
)
while True:
run_status = openai.beta.threads.runs.retrieve(thread_id=thread_id, run_id=run.id)
if run_status.status == "requires_action":
tool_calls = run_status.required_action.submit_tool_outputs.tool_calls
tool_outputs = []
for tool_call in tool_calls:
tool_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
if tool_name == "execute_shell_command":
vysledek = spust_prikaz(arguments["command"])
elif tool_name == "create_script":
vysledek = vytvor_skript(arguments["file_name"], arguments["content"])
else:
vysledek = "Neznámá funkce."
tool_outputs.append({
"tool_call_id": tool_call.id,
"output": vysledek
})
openai.beta.threads.runs.submit_tool_outputs(
thread_id=thread_id,
run_id=run.id,
tool_outputs=tool_outputs
)
elif run_status.status == "completed":
break
time.sleep(1)
messages = openai.beta.threads.messages.list(thread_id=thread_id)
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):
return response.strip().lower().startswith("navrhovaný příkaz:")
def get_first_command_proposal(response):
"""Vrátí první řádek, který začíná 'Navrhovaný příkaz:'; jinak None."""
lines = response.splitlines()
for line in lines:
if line.strip().lower().startswith("navrhovaný příkaz:"):
return line
return None
# === 5. Funkce pro spouštění příkazů a report ===
def spust_prikaz(command):
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):
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):
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. Vylepšené interaktivní rozhraní s curses ===
def main_curses(stdscr):
curses.start_color()
curses.use_default_colors()
curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) # Hlavička
curses.init_pair(2, curses.COLOR_WHITE, -1) # Chat
curses.init_pair(3, curses.COLOR_GREEN, -1) # Výstup
curses.init_pair(4, curses.COLOR_YELLOW, -1) # Vstup
curses.init_pair(5, curses.COLOR_CYAN, -1) # Spinner
curses.init_pair(6, curses.COLOR_BLACK, curses.COLOR_WHITE) # Oddělovač
curses.curs_set(1)
stdscr.nodelay(False)
stdscr.clear()
height, width = stdscr.getmaxyx()
header_height = 3
prompt_height = 2
left_width = width // 2
right_width = width - left_width - 1 # oddělovač má 1 sloupec
chat_height = height - header_height - prompt_height
header_win = curses.newwin(header_height, left_width, 0, 0)
chat_win = curses.newwin(chat_height, left_width, header_height, 0)
prompt_win = curses.newwin(prompt_height, width, height - prompt_height, 0)
output_win = curses.newwin(height - prompt_height, right_width, 0, left_width + 1)
divider_win = curses.newwin(height - prompt_height, 1, 0, left_width)
header_win.bkgd(' ', curses.color_pair(1))
chat_win.bkgd(' ', curses.color_pair(2))
prompt_win.bkgd(' ', curses.color_pair(4))
output_win.bkgd(' ', curses.color_pair(3))
divider_win.bkgd(' ', curses.color_pair(6))
d_height, _ = divider_win.getmaxyx()
for y in range(d_height):
try:
divider_win.addch(y, 0, curses.ACS_VLINE)
except curses.error:
pass
divider_win.refresh()
header_text = [
"Vítejte v Robovojtikovi!",
"Zadejte dotaz, příkaz (prefix 'cmd:'), 'automat' nebo 'skript: nazev; obsah'.",
"Pro ukončení zadejte 'vypni' nebo 'exit'."
]
for idx, line in enumerate(header_text):
header_win.addstr(idx, 1, line)
header_win.refresh()
chat_win.scrollok(True)
output_win.scrollok(True)
prompt_win.scrollok(True)
output_win.box()
output_win.refresh()
# Funkce pro spinner v hlavičce (uprostřed)
def spinner_func(ch):
mid_x = left_width // 2 - 5
header_win.addstr(1, mid_x, f"Čekám... {ch}", curses.color_pair(5) | curses.A_BOLD)
header_win.refresh()
chat_history = []
def add_to_chat(text):
chat_history.append(text)
chat_win.addstr(text + "\n")
chat_win.refresh()
add_to_chat("Historie chatu:")
while True:
prompt_win.erase()
prompt_win.addstr(">> ", curses.A_BOLD)
prompt_win.refresh()
curses.echo()
try:
user_input = prompt_win.getstr().decode("utf-8").strip()
except:
continue
curses.noecho()
if not user_input:
continue
add_to_chat("Ty: " + user_input)
if user_input.lower() in ["vypni", "exit"]:
add_to_chat("Ukončuji Robovojtíka...")
time.sleep(1)
break
if user_input.lower() == "automat":
global automode
automode = not automode
stav = "zapnut" if automode else "vypnut"
add_to_chat(f"Automód byl nyní {stav}.")
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)
add_to_chat(vysledek)
except Exception as e:
add_to_chat(f"Chyba při vytváření skriptu: {str(e)}")
continue
if user_input.startswith("cmd:"):
command = user_input[4:].strip()
add_to_chat(f"Rozpoznán přímý příkaz: {command}")
if not automode:
prompt_win.erase()
prompt_win.addstr("Spustit tento příkaz? (y/n): ", curses.A_BOLD)
prompt_win.refresh()
potvrzeni = prompt_win.getstr().decode("utf-8").strip().lower()
add_to_chat("Ty: " + potvrzeni)
if potvrzeni != "y":
add_to_chat("Příkaz nebyl spuštěn.")
continue
output, response = run_command_locally_and_report(command)
output_win.erase()
output_win.addstr(1, 1, output)
output_win.box()
output_win.refresh()
add_to_chat("Robovojtík odpovídá:\n" + response)
continue
assistant_response = volani_asistenta(user_input, spinner_func=spinner_func)
add_to_chat("Robovojtík odpovídá:\n" + assistant_response)
proposal_line = get_first_command_proposal(assistant_response)
if proposal_line:
navrhovany_prikaz = proposal_line[len("Navrhovaný příkaz:"):].strip()
add_to_chat(f"Navrhovaný příkaz: {navrhovany_prikaz}")
if not automode:
prompt_win.erase()
prompt_win.addstr("Spustit tento příkaz? (y/n): ", curses.A_BOLD)
prompt_win.refresh()
potvrzeni = prompt_win.getstr().decode("utf-8").strip().lower()
add_to_chat("Ty: " + potvrzeni)
if potvrzeni != "y":
add_to_chat("Příkaz nebyl spuštěn.")
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()
add_to_chat("Robovojtík odpovídá:\n" + response)
prompt_win.erase()
prompt_win.refresh()
def main():
global log_enabled, listener, log_queue
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()
if args.log:
log_enabled = True
log_queue = queue.Queue(-1)
qh = logging.handlers.QueueHandler(log_queue)
logger.addHandler(qh)
logger.setLevel(logging.DEBUG)
fh = logging.FileHandler("robovojtik.log")
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__":
main()

78
shell_functions.py Normal file
View File

@ -0,0 +1,78 @@
"""
Module: shell_functions.py
----------------------------
Obsahuje funkce pro vykonávání lokálních shellových příkazů a tvorbu skriptů.
Přidána funkce read_file_content pro načtení souboru.
"""
import subprocess
import logging
import os
logger = logging.getLogger("robovojtik.shell_functions")
MAX_FILE_SIZE = 10 * 1024 # 10 kB
def clean_command(command):
"""
Odstraní backticky a nepotřebné mezery z příkazu.
"""
return command.replace("`", "").strip()
def spust_prikaz(command):
"""
Spustí příkaz lokálně a vrátí jeho výstup.
V případě chyby vrací chybovou hlášku.
"""
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:
logger.exception("Chyba při vykonávání příkazu.")
return f"Chyba při vykonávání příkazu:\n{e.output}"
except Exception as e:
logger.exception("Obecná chyba při vykonávání příkazu.")
return f"Chyba při vykonávání příkazu: {str(e)}"
def vytvor_skript(nazev, obsah):
"""
Zapíše obsah do souboru s daným názvem a nastaví jej jako spustitelný.
"""
try:
with open(nazev, "w", encoding="utf-8") 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:
logger.exception("Chyba při tvorbě skriptu.")
return f"Nastala chyba při tvorbě skriptu: {str(e)}"
def read_file_content(path: str) -> str:
"""
Načte soubor na adrese 'path' do max. 10 kB.
Pokud je soubor větší, vrátí jen 10 kB a poznámku.
"""
try:
if not os.path.isfile(path):
return f"Chyba: Soubor '{path}' neexistuje nebo není souborem."
size = os.path.getsize(path)
partial = (size > MAX_FILE_SIZE)
with open(path, "rb") as f:
data = f.read(MAX_FILE_SIZE)
text = data.decode("utf-8", errors="replace")
if partial:
text += "\n\n[POZOR: Soubor byl delší než 10 kB, zobrazujeme pouze prvních 10 kB!]"
return text
except Exception as e:
logger.exception("Chyba při čtení souboru")
return f"Chyba při čtení souboru: {str(e)}"
def run_command_locally(command):
"""
Spustí příkaz lokálně a vrátí jeho výstup.
"""
command = clean_command(command)
output = spust_prikaz(command)
return output

109
system_prompt Normal file
View File

@ -0,0 +1,109 @@
Jsi Linuxový shell asistent jménem Robovojtík. Tvým úkolem je pomáhat uživateli vykonávat příkazy v systému, klidně složitějšího charakteru (pipe, grep...), analyzovat soubory a usnadnit práci se skripty. Můžeš také generovat skripty podle zadání a analyzovat obsah souborů.
🔹 **Obecné chování**:
- Pokud se tě uživatel ptá na cokoli ohledně systému, souborů nebo příkazů, nejprve mu **vysvětli řešení a navrhni vhodný příkaz**.
- **Nikdy neprováděj příkaz automaticky**, pokud není zapnutý **automatický režim (automód)**.
- Pokud je **automód aktivní**, příkaz provedeš rovnou a oznámíš výsledek.
- Pokud **automód není aktivní**, vždy čekáš na potvrzení uživatele před spuštěním příkazu.
- **Používáš Markdown formátování** pro odpovědi, aby byly přehledné. Používej i barvy, píšeš na černé pozadí.
---
## 🔹 **Práce se soubory**
- Pokud se tě uživatel zeptá na obsah souboru, zavoláš funkci **read_file**.
- Umíš **načíst soubor do velikosti 10 kB**. Pokud je větší, načteš jen prvních 10 kB a připojíš upozornění:
`⚠️ Upozornění: Soubor byl delší než 10 kB, zobrazujeme pouze prvních 10 kB!`
- Pokud je soubor **binární**, odpovíš:
`❌ Tento soubor nelze přečíst jako text.`
- Po načtení souboru se ho pokusíš **analyzovat**, například:
- Pokud jde o skript, vysvětlíš, co dělá.
- Pokud jde o konfigurační soubor, shrneš jeho nastavení.
---
## 🔹 **Vykonávání příkazů**
- Pokud uživatel **zadá dotaz**, navrhneš odpovídající shellový příkaz.
**Odpověď začínáš prefixem**:
🖥️ Navrhovaný příkaz: ls -la
Analyzuj následující výstup příkazu. Neposkytuj žádné návrhy příkazů, jen shrň výsledky a uveď komentář.
markdown
Zkopírovat
Upravit
- Pokud je automód vypnutý, vždy čekáš na odpověď „**y**“ nebo „**n**“.
- Pokud se příkaz vykoná, **analyzuješ výstup** a dáš uživateli zpětnou vazbu.
- Pokud dojde k chybě, pokusíš se pomoci uživateli ji opravit.
---
## 🔹 **Práce se skripty**
- Uživatel může chtít vytvořit skript. Pomůžeš mu tím, že:
- **Navrhneš kód skriptu** na základě popisu uživatele.
- Zeptáš se na jméno souboru, pokud ho uživatel nezadal.
- Po potvrzení vytvoříš soubor a nastavíš ho jako spustitelný.
- **Pokud uživatel zadá:**
`"Vytvoř skript, který vypíše nejnovější soubor"`
**Odpovíš:**
📝 Navrhovaný skript:
bash
Zkopírovat
Upravit
```bash
#!/bin/bash
ls -t | head -n 1
"Chceš tento skript vytvořit?"
Po potvrzení zavoláš funkci create_script.
🔹 Automód (automatický režim)
Pokud je zapnutý automód, příkazy ihned provádíš bez potvrzení.
Po provedení popíšeš výsledek.
Pokud je vypnutý automód, vždy požádáš o potvrzení.
🔹 Odpovědi a formátování
Používáš Markdown, abys zpřehlednil odpovědi:
Tučné písmo pro důležité části.
🔹 Emoji pro vizuální odlišení sekcí.
Bloky kódu pro příkazy a skripty:
bash
Zkopírovat
Upravit
ls -la
Pokud výstup obsahuje chybu, použiješ:
yaml
Zkopírovat
Upravit
❌ Chyba: Příkaz nebyl úspěšný. Možné řešení: ...
Příklad interakce
Uživatel:
„Vypiš mi obsah složky.“
Robovojtík:
bash
Zkopírovat
Upravit
🖥️ Navrhovaný příkaz: ls -l
Chceš tento příkaz vykonat? (y/n)
(Pokud uživatel odpoví y)
Robovojtík:
sql
Zkopírovat
Upravit
📄 Výpis složky:
total 4
-rw-r--r-- 1 user user 120 Mar 20 12:34 config.ini
-rwxr-xr-x 1 user user 204 Mar 20 12:35 script.sh
✅ Příkaz proběhl úspěšně.
🛠️ Tvůj hlavní úkol je:
Analyzovat dotazy a nabídnout nejlepší možné řešení.
Používat funkce (execute_shell_command, create_script, read_file).
Vždy dávat smysluplnou zpětnou vazbu.
Používat formátování Markdown pro přehledné odpovědi.
Respektovat automód v automatickém režimu pracuješ sám, jinak čekáš na potvrzení.
Pokud zpráva začíná "Výsledek příkazu:", uživatel žádá pouze stručné shrnutí a komentář bez jakéhokoliv návrhu příkazů.

262
ui.py Normal file
View File

@ -0,0 +1,262 @@
"""
Module: ui.py
-------------
Obsahuje interaktivní rozhraní Robovojtíka založené na knihovně curses.
Implementuje barevné oddělení, trvalou hlavičku, historii chatu,
spinner pro indikaci čekání a vstupní oblast. Také prefix 'readfile:'.
"""
import curses
import time
import threading
import logging
import re
import api_interface
import shell_functions
import markdown_parser
logger = logging.getLogger("robovojtik.ui")
# Globální funkce pro integraci s main.py
FUNCTIONS = []
def set_functions(funcs):
global FUNCTIONS
FUNCTIONS = funcs
automode = False
def main_curses(stdscr):
curses.start_color()
curses.use_default_colors()
curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) # Hlavička
curses.init_pair(2, curses.COLOR_WHITE, -1) # Chat
curses.init_pair(3, curses.COLOR_GREEN, -1) # Výstup
curses.init_pair(4, curses.COLOR_YELLOW, -1) # Vstup
curses.init_pair(5, curses.COLOR_CYAN, -1) # Spinner
curses.init_pair(6, curses.COLOR_BLACK, curses.COLOR_WHITE) # Oddělovač
curses.curs_set(1)
stdscr.nodelay(False)
stdscr.clear()
height, width = stdscr.getmaxyx()
header_height = 3
prompt_height = 2
left_width = width // 2
right_width = width - left_width - 1
chat_height = height - header_height - prompt_height
header_win = curses.newwin(header_height, left_width, 0, 0)
chat_win = curses.newwin(chat_height, left_width, header_height, 0)
prompt_win = curses.newwin(prompt_height, width, height - prompt_height, 0)
output_win = curses.newwin(height - prompt_height, right_width, 0, left_width + 1)
divider_win = curses.newwin(height - prompt_height, 1, 0, left_width)
header_win.bkgd(' ', curses.color_pair(1))
chat_win.bkgd(' ', curses.color_pair(2))
prompt_win.bkgd(' ', curses.color_pair(4))
output_win.bkgd(' ', curses.color_pair(3))
divider_win.bkgd(' ', curses.color_pair(6))
d_height, _ = divider_win.getmaxyx()
for y in range(d_height):
try:
divider_win.addch(y, 0, curses.ACS_VLINE)
except curses.error:
pass
divider_win.refresh()
header_text = [
"Vítejte v Robovojtikovi!",
"Zadejte dotaz, příkaz (prefix 'cmd:'), 'automat', 'readfile:' apod.",
"Pro ukončení zadejte 'vypni' nebo 'exit'."
]
max_chars = max(0, left_width - 2)
for idx, line in enumerate(header_text):
try:
header_win.addnstr(idx, 1, line, max_chars)
except curses.error:
pass
header_win.refresh()
chat_win.scrollok(True)
output_win.scrollok(True)
prompt_win.scrollok(True)
output_win.box()
output_win.refresh()
def spinner_func(ch):
mid_x = left_width // 2 - 5
try:
header_win.addnstr(1, mid_x, f"Čekám... {ch}", left_width - mid_x - 1,
curses.color_pair(5) | curses.A_BOLD)
header_win.refresh()
except curses.error:
pass
def add_to_chat(text):
segments = markdown_parser.parse_markdown(text)
for seg_text, seg_attr in segments:
try:
chat_win.addstr(seg_text, seg_attr)
except curses.error:
pass
chat_win.refresh()
add_to_chat("Historie chatu:")
global automode
while True:
prompt_win.erase()
try:
prompt_win.addstr(">> ", curses.A_BOLD)
except curses.error:
pass
prompt_win.refresh()
curses.echo()
try:
user_input = prompt_win.getstr().decode("utf-8").strip()
except Exception:
continue
curses.noecho()
if not user_input:
continue
add_to_chat("Ty: " + user_input)
# Ukončení
if user_input.lower() in ["vypni", "exit"]:
add_to_chat("Ukončuji Robovojtíka...")
time.sleep(1)
break
# Přepnutí automódu
if user_input.lower() == "automat":
automode = not automode
stav = "zapnut" if automode else "vypnut"
add_to_chat(f"Automód byl nyní {stav}.")
continue
# readfile:
if user_input.lower().startswith("readfile:"):
path = user_input[9:].strip()
add_to_chat(f"Načítám soubor: {path}")
from shell_functions import read_file_content
content = read_file_content(path)
add_to_chat(f"Obsah souboru:\n{content}")
assistant_response = api_interface.volani_asistenta(
f"Analyzuj obsah tohoto souboru:\n{content}",
spinner_func=spinner_func
)
add_to_chat("Robovojtík odpovídá:\n" + assistant_response)
continue
# Přímý příkaz cmd:
if user_input.startswith("cmd:"):
command = user_input[4:].strip()
add_to_chat(f"Rozpoznán příkaz: {command}")
if not automode:
prompt_win.erase()
try:
prompt_win.addstr("Spustit tento příkaz? (y/n): ", curses.A_BOLD)
except curses.error:
pass
prompt_win.refresh()
try:
potvrzeni = prompt_win.getstr().decode("utf-8").strip().lower()
except Exception:
potvrzeni = ""
curses.flushinp() # vyprázdní vstupní buffer
add_to_chat("Ty: " + potvrzeni)
if potvrzeni not in ("y", "yes", "ano"):
add_to_chat("Příkaz nebyl spuštěn.")
continue
output = shell_functions.run_command_locally(command)
output_win.erase()
try:
lines = output.splitlines()
y = 1
for line in lines:
output_win.addstr(y, 1, line)
y += 1
output_win.box()
output_win.refresh()
except curses.error:
pass
summary_prompt = (f"Analyzuj následující výstup příkazu. Neposkytuj žádné návrhy příkazů, "
f"jen shrň výsledky a uveď komentář:\n{output}")
assistant_summary = api_interface.volani_asistenta(summary_prompt, spinner_func=spinner_func)
add_to_chat("Robovojtík shrnuje výstup:\n" + assistant_summary)
continue
# Ostatní dotazy -> Asistent
assistant_response = api_interface.volani_asistenta(user_input, spinner_func=spinner_func)
add_to_chat("Robovojtík odpovídá:\n" + assistant_response)
# Pokud asistent navrhne příkaz, extrahujeme jej
match = re.search(r"`([^`]+)`", assistant_response)
if match:
navrhovany_prikaz = match.group(1)
elif assistant_response.strip().lower().startswith("navrhovaný příkaz:"):
lines = assistant_response.splitlines()
proposal_line = lines[0]
navrhovany_prikaz = proposal_line[len("navrhovaný příkaz:"):].strip()
else:
navrhovany_prikaz = None
if navrhovany_prikaz:
add_to_chat(f"Navrhovaný příkaz: {navrhovany_prikaz}")
if not automode:
prompt_win.erase()
try:
prompt_win.addstr("Spustit tento příkaz? (y/n): ", curses.A_BOLD)
except curses.error:
pass
prompt_win.refresh()
try:
potvrzeni = prompt_win.getstr().decode("utf-8").strip().lower()
except Exception:
potvrzeni = ""
curses.flushinp() # vyprázdní vstupní buffer
add_to_chat("Ty: " + potvrzeni)
if potvrzeni not in ("y", "yes", "ano"):
add_to_chat("Příkaz nebyl spuštěn.")
continue
output = shell_functions.run_command_locally(navrhovany_prikaz)
output_win.erase()
try:
lines = output.splitlines()
y = 1
for line in lines:
output_win.addstr(y, 1, line)
y += 1
output_win.box()
output_win.refresh()
except curses.error:
pass
summary_prompt = (
f"Výsledek příkazu:\n{output}\n\n"
"Prosím, stručně shrň výše uvedený výstup příkazu a "
"uveď pouze komentář bez návrhu dalších příkazů."
)
assistant_summary = api_interface.volani_asistenta(summary_prompt, spinner_func=spinner_func)
add_to_chat("Robovojtík shrnuje výstup:\n" + assistant_summary)
continue
prompt_win.erase()
prompt_win.refresh()
def main_ui():
try:
curses.wrapper(main_curses)
except Exception as e:
logger.exception("Neočekávaná chyba v curses wrapperu.")
print(f"Chyba: {str(e)}")