diff --git "a/\"b/mock_data\\\\sessions.json\"" "b/\"b/mock_data\\\\sessions.json\"" new file mode 100644 index 00000000..5b5b7fbb --- /dev/null +++ "b/\"b/mock_data\\\\sessions.json\"" @@ -0,0 +1,142 @@ +[ + { + "id": "e4d32aee-1f40-4c48-a2fd-fe8221358ab2", + "total_interactions": 30, + "successful_interactions": 20, + "total_followed": 10, + "total_likes": 10, + "total_comments": 10, + "total_pm": 10, + "total_watched": 10, + "total_unfollowed": 10, + "total_scraped": {}, + "start_time": "2023-12-29 17:56:22.612008", + "finish_time": "2023-12-29 19:57:34.465153", + "args": {}, + "profile": { + "posts": 4, + "followers": 200, + "following": 1011 + } + }, + { + "id": "38341cbf-589d-49ab-98c6-b5b54558b486", + "total_interactions": 30, + "successful_interactions": 20, + "total_followed": 10, + "total_likes": 10, + "total_comments": 10, + "total_pm": 10, + "total_watched": 10, + "total_unfollowed": 10, + "total_scraped": {}, + "start_time": "2024-01-01 11:49:46.405981", + "finish_time": "2024-01-01 12:19:47.342904", + "args": {}, + "profile": { + "posts": 4, + "followers": 222, + "following": 1008 + } + }, + { + "id": "e4d32aee-1f40-4c48-a2fd-fe8221358ab2", + "total_interactions": 20, + "successful_interactions": 5, + "total_followed": 1, + "total_likes": 1, + "total_comments": 1, + "total_pm": 1, + "total_watched": 1, + "total_unfollowed": 1, + "total_scraped": {}, + "start_time": "2024-01-01 12:03:57.633619", + "finish_time": "None", + "args": {}, + "profile": { + "posts": 4, + "followers": 225, + "following": 1007 + } + }, + { + "id": "ca81fc51-9e5c-4f83-a2bc-7ccbc0c6fef1", + "total_interactions": 300, + "successful_interactions": 10, + "total_followed": 5, + "total_likes": 5, + "total_comments": 5, + "total_pm": 5, + "total_watched": 5, + "total_unfollowed": 5, + "total_scraped": {}, + "start_time": "2024-01-01 19:45:38.757304", + "finish_time": "2024-01-01 20:55:01.146696", + "args": {}, + "profile": { + "posts": 4, + "followers": 230, + "following": 1009 + } + }, + { + "id": "631e137f-3f3e-4377-a455-118225a03690", + "total_interactions": 10, + "successful_interactions": 10, + "total_followed": 5, + "total_likes": 5, + "total_comments": 5, + "total_pm": 5, + "total_watched": 5, + "total_unfollowed": 5, + "total_scraped": {}, + "start_time": "2024-01-02 19:36:38.578977", + "finish_time": "2024-01-02 19:57:41.298307", + "args": {}, + "profile": { + "posts": 4, + "followers": 242, + "following": 1009 + } + }, + { + "id": "f4a76d95-7edb-4930-a147-897c0755826b", + "total_interactions": 30, + "successful_interactions": 15, + "total_followed": 5, + "total_likes": 5, + "total_comments": 5, + "total_pm": 5, + "total_watched": 5, + "total_unfollowed": 5, + "total_scraped": {}, + "start_time": "2024-01-02 21:37:08.331302", + "finish_time": "2024-01-02 21:48:00.768777", + "args": {}, + "profile": { + "posts": 4, + "followers": 247, + "following": 1011 + } + }, + { + "id": "f76d24ed-1fad-4d03-8a5a-af367ce59d2f", + "total_interactions": 30, + "successful_interactions": 20, + "total_followed": 10, + "total_likes": 10, + "total_comments": 10, + "total_pm": 10, + "total_watched": 10, + "total_unfollowed": 10, + "total_scraped": {}, + "start_time": "2024-01-02 21:56:57.612008", + "finish_time": "2024-01-02 22:57:44.465153", + "args": {}, + "profile": { + "posts": 4, + "followers": 250, + "following": 1011 + } + } +] diff --git a/GramAddict/plugins/llama_image_chat.py b/GramAddict/plugins/llama_image_chat.py new file mode 100644 index 00000000..61a18853 --- /dev/null +++ b/GramAddict/plugins/llama_image_chat.py @@ -0,0 +1,433 @@ +import base64 +import json +import logging +import os +import re +import socket +from pathlib import Path +from typing import Iterable, List, Optional, Sequence + +import requests + +from GramAddict.core.plugin_loader import Plugin + +logger = logging.getLogger(__name__) + + +def encode_image_to_base64(path: Path) -> str: + """Return a base64 string representation of the image at *path*.""" + + with path.open("rb") as image_file: + encoded = base64.b64encode(image_file.read()) + return encoded.decode("utf-8") + + +def build_chat_messages( + message: str, encoded_images: Iterable[str], history: Optional[List[dict]] = None +) -> List[dict]: + """Compose a chat payload compatible with multimodal LLaMA endpoints.""" + + messages: List[dict] = list(history) if history else [] + content = [{"type": "text", "text": message}] + for encoded_image in encoded_images: + content.append({"type": "image", "image": encoded_image}) + messages.append({"role": "user", "content": content}) + return messages + + +SECRET_KEYWORDS = ( + "api_key", + "api-key", + "apikey", + "token", + "secret", + "password", + "key", + "credential", +) +SECRET_VALUE_PATTERN = re.compile(r"(? str: + """Mask potential secrets like API keys or tokens in *text*.""" + + if not text: + return text + + def _mask_keyword(match: re.Match[str]) -> str: + key = match.group("keyword") + sep = match.group("separator") + leading_ws = match.group("leading_ws") or "" + trailing_ws = match.group("trailing_ws") or "" + raw_value = match.group("value") + + quote_prefix = "" + quote_suffix = "" + core_value = raw_value + if len(raw_value) >= 2 and raw_value[0] == raw_value[-1] and raw_value[0] in {'"', "'"}: + quote_prefix = raw_value[0] + quote_suffix = raw_value[-1] + core_value = raw_value[1:-1] + + if not core_value: + masked_core = "" + elif len(core_value) > 8: + masked_core = f"{core_value[:4]}***{core_value[-4:]}" + else: + masked_core = "***" + + masked_value = f"{quote_prefix}{masked_core}{quote_suffix}" + return f"{key}{leading_ws}{sep}{trailing_ws}{masked_value}" + + keyword_pattern = re.compile( + r"(?" + "|".join(re.escape(keyword) for keyword in SECRET_KEYWORDS) + r")" + r"(?P\s*)" + r"(?P[:=])" + r"(?P\s*)" + r"(?P[^\s,;]+)", + flags=re.IGNORECASE, + ) + + text = keyword_pattern.sub(_mask_keyword, text) + + def _mask_long_value(match: re.Match[str]) -> str: + value = match.group(1) + if "***" in value: + return value + if len(value) <= 8: + return "***" + return f"{value[:4]}***{value[-4:]}" + + return SECRET_VALUE_PATTERN.sub(_mask_long_value, text) + + +def sanitize_messages(messages: Sequence[dict]) -> List[dict]: + """Return a deep copy of *messages* with text entries masked for secrets.""" + + sanitized: List[dict] = [] + for message in messages: + content_items = [] + for item in message.get("content", []): + if item.get("type") == "text": + content_items.append({**item, "text": mask_secrets(str(item.get("text", "")))}) + else: + content_items.append(dict(item)) + sanitized.append({**message, "content": content_items}) + return sanitized + + +DEFAULT_SECURITY_PORTS = (22, 80, 443, 8080, 8443) + + +def scan_network( + hosts: Iterable[str], ports: Iterable[int], timeout: float = 0.5 +) -> dict[str, list[int]]: + """Attempt to connect to *ports* on each host and return open ports.""" + + open_ports: dict[str, list[int]] = {} + ports_list = sorted({int(port) for port in ports if int(port) > 0}) + for host in hosts: + host_open_ports: list[int] = [] + for port in ports_list: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.settimeout(timeout) + try: + result = sock.connect_ex((host, port)) + except OSError: + continue + if result == 0: + host_open_ports.append(port) + if host_open_ports: + open_ports[host] = host_open_ports + return open_ports + + +class LlamaImageChat(Plugin): + """Send text prompts with image attachments to a LLaMA-compatible endpoint.""" + + def __init__(self): + super().__init__() + self.description = ( + "Send prompts, images, and maintain conversation history with a" + " LLaMA-compatible API." + ) + self.arguments = [ + { + "arg": "--llama-chat", + "help": "Send a request to a LLaMA-compatible endpoint with optional images.", + "action": "store_true", + "operation": True, + }, + { + "arg": "--llama-images", + "nargs": "+", + "help": "Path(s) to images that should be attached to the request.", + "metavar": "IMAGE", + "default": None, + }, + { + "arg": "--llama-message", + "nargs": None, + "help": "Prompt text that should be sent to the assistant.", + "metavar": "TEXT", + "default": None, + }, + { + "arg": "--llama-prompt-file", + "nargs": None, + "help": "Path to a file containing the prompt text.", + "metavar": "FILE", + "default": None, + }, + { + "arg": "--llama-model", + "nargs": None, + "help": "Model name to use when talking to the endpoint (default: llama3).", + "metavar": "MODEL", + "default": "llama3", + }, + { + "arg": "--llama-endpoint", + "nargs": None, + "help": "HTTP endpoint for the chat API (default: http://localhost:11434/api/chat).", + "metavar": "URL", + "default": "http://localhost:11434/api/chat", + }, + { + "arg": "--llama-history-file", + "nargs": None, + "help": "Optional JSON file storing the conversation history.", + "metavar": "FILE", + "default": None, + }, + { + "arg": "--llama-output-file", + "nargs": None, + "help": "Optional path to store the assistant response.", + "metavar": "FILE", + "default": None, + }, + { + "arg": "--llama-scan-hosts", + "nargs": "+", + "help": "Hostnames or IPs that should be scanned for open ports.", + "metavar": "HOST", + "default": None, + }, + { + "arg": "--llama-port-range", + "nargs": 2, + "type": int, + "help": "Port range (inclusive) used during security scans.", + "metavar": ("START", "END"), + "default": None, + }, + { + "arg": "--llama-security-report", + "nargs": None, + "help": "Optional JSON file storing results of the security sweep.", + "metavar": "FILE", + "default": None, + }, + ] + + def run(self, device, configs, storage, sessions, profile_filter, plugin): + args = configs.args + if not getattr(args, "llama_chat", False): + logger.debug("LLaMA chat argument not enabled; skipping execution.") + return + + security_report = self._security_sweep(args) + if security_report: + logger.info("Security sweep completed: %s", json.dumps(security_report, indent=2)) + + message = self._get_prompt(args) + if not message: + logger.error("No prompt provided for the LLaMA chat request.") + return + + images = self._load_images(args.llama_images) + history, history_path = self._load_history(args.llama_history_file) + + payload = { + "model": args.llama_model or "llama3", + "messages": build_chat_messages(message, images, history), + } + + sanitized_payload = dict(payload) + sanitized_payload["messages"] = sanitize_messages(payload["messages"]) + logger.debug( + "Sending LLaMA payload: %s", + json.dumps(sanitized_payload, ensure_ascii=False, indent=2)[:500], + ) + response_text = self._send_request(args.llama_endpoint, payload) + if response_text is None: + return + + sanitized_response = mask_secrets(response_text) + self._persist_history(history, history_path, message, images, response_text) + self._write_output(args.llama_output_file, sanitized_response) + logger.info("LLaMA response: %s", sanitized_response) + + def wyy( + self, message: str, role: str = "assistant", images: Optional[Iterable[str]] = None + ) -> dict: + """Create a conversation entry following the multimodal chat schema.""" + + content = [{"type": "text", "text": message}] + for encoded_image in images or []: + content.append({"type": "image", "image": encoded_image}) + return {"role": role, "content": content} + + def _get_prompt(self, args) -> Optional[str]: + if args.llama_message: + return str(args.llama_message) + if not args.llama_prompt_file: + return None + path = Path(args.llama_prompt_file) + try: + return path.read_text(encoding="utf-8").strip() + except FileNotFoundError: + logger.error("Prompt file '%s' does not exist.", path) + except OSError as exc: + logger.error("Failed to read prompt file '%s': %s", path, exc) + return None + + def _load_images(self, images: Optional[Iterable[str]]) -> List[str]: + encoded_images: List[str] = [] + if not images: + return encoded_images + for image_path in images: + path = Path(image_path) + if not path.exists(): + logger.error("Image file '%s' not found. Skipping.", image_path) + continue + try: + encoded_images.append(encode_image_to_base64(path)) + except OSError as exc: + logger.error("Unable to read image '%s': %s", image_path, exc) + return encoded_images + + def _load_history( + self, history_file: Optional[str] + ) -> tuple[Optional[List[dict]], Optional[Path]]: + if not history_file: + return None, None + path = Path(history_file) + if not path.exists(): + path.parent.mkdir(parents=True, exist_ok=True) + return [], path + try: + with path.open("r", encoding="utf-8") as history_stream: + data = json.load(history_stream) + if isinstance(data, list): + return data, path + logger.warning("History file '%s' is not a list. Resetting history.", path) + return [], path + except json.JSONDecodeError as exc: + logger.error("Failed to decode history file '%s': %s", path, exc) + except OSError as exc: + logger.error("Failed to read history file '%s': %s", path, exc) + return [], path + + def _send_request(self, endpoint: str, payload: dict) -> Optional[str]: + try: + response = requests.post(endpoint, json=payload, timeout=60) + response.raise_for_status() + except requests.RequestException as exc: + logger.error("Failed to send request to '%s': %s", endpoint, exc) + return None + + try: + data = response.json() + except ValueError: + logger.error("Endpoint '%s' did not return valid JSON.", endpoint) + return None + + # Handle both OpenAI-compatible and Ollama style responses. + if isinstance(data, dict): + if "message" in data and isinstance(data["message"], dict): + return str(data["message"].get("content", "")).strip() + if "response" in data: + return str(data.get("response", "")).strip() + if "choices" in data: + choices = data["choices"] + if isinstance(choices, list) and choices: + message = choices[0].get("message", {}) + if isinstance(message, dict): + return str(message.get("content", "")).strip() + logger.error( + "Could not find an assistant message in the response from '%s'.", endpoint + ) + return None + + def _persist_history( + self, + history: Optional[List[dict]], + history_path: Optional[Path], + message: str, + images: List[str], + response_text: str, + ) -> None: + if history_path is None: + return + updated_history = list(history or []) + updated_history.append( + self.wyy(mask_secrets(message), role="user", images=images) + ) + updated_history.append(self.wyy(mask_secrets(response_text), role="assistant")) + try: + with history_path.open("w", encoding="utf-8") as history_stream: + json.dump(updated_history, history_stream, ensure_ascii=False, indent=2) + except OSError as exc: + logger.error( + "Failed to write history file '%s': %s", history_path, exc + ) + + def _write_output(self, output_file: Optional[str], response_text: str) -> None: + if not output_file: + return + path = Path(output_file) + path.parent.mkdir(parents=True, exist_ok=True) + try: + path.write_text(response_text, encoding="utf-8") + except OSError as exc: + logger.error("Failed to write response file '%s': %s", path, exc) + + def _security_sweep(self, args) -> Optional[dict]: + hosts = getattr(args, "llama_scan_hosts", None) + if not hosts: + return None + + port_range = getattr(args, "llama_port_range", None) + if port_range and len(port_range) == 2: + start, end = sorted(int(port) for port in port_range) + ports = range(max(start, 1), end + 1) + else: + ports = DEFAULT_SECURITY_PORTS + + open_ports = scan_network(hosts, ports) + sanitized_env: dict[str, str] = {} + for key, value in os.environ.items(): + if SECRET_KEYWORD_BOUNDARY.search(key) or SECRET_VALUE_PATTERN.search(str(value)): + sanitized_env[key] = mask_secrets(str(value)) + report = { + "network": open_ports, + "environment": sanitized_env, + } + + report_path = getattr(args, "llama_security_report", None) + if report_path: + try: + path = Path(report_path) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(report, indent=2), encoding="utf-8") + except OSError as exc: + logger.error("Failed to write security report '%s': %s", report_path, exc) + + return report if (open_ports or sanitized_env) else {} diff --git a/GramAddict/plugins/telegram.py b/GramAddict/plugins/telegram.py index 675b139a..940a0d54 100644 --- a/GramAddict/plugins/telegram.py +++ b/GramAddict/plugins/telegram.py @@ -1,7 +1,7 @@ import json import logging from datetime import datetime -from typing import Optional +from typing import List, Optional import requests import yaml @@ -12,13 +12,18 @@ logger = logging.getLogger(__name__) -def load_sessions(username) -> Optional[dict]: +def load_sessions(username) -> Optional[List[dict]]: try: - with open(f"accounts/{username}/sessions.json") as json_data: + with open( + f"accounts/{username}/sessions.json", "r", encoding="utf-8" + ) as json_data: return json.load(json_data) except FileNotFoundError: logger.error("No session data found. Skipping report generation.") return None + except json.JSONDecodeError as exc: + logger.error(f"Invalid session data for {username}: {exc}") + return None def load_telegram_config(username) -> Optional[dict]: diff --git a/INSTALLATION.md b/INSTALLATION.md new file mode 100644 index 00000000..8e7632b4 --- /dev/null +++ b/INSTALLATION.md @@ -0,0 +1,299 @@ +# Instrukcja instalacji + +Ten dokument opisuje proces instalacji wszystkich wymaganych programów i zależności dla projektu GramAddict z rozszerzeniem LLaMA. + +## Spis treści + +- [Wymagania systemowe](#wymagania-systemowe) +- [Instalacja Ollama](#instalacja-ollama) +- [Instalacja zależności Python](#instalacja-zależności-python) +- [Konfiguracja rozszerzenia LLaMA](#konfiguracja-rozszerzenia-llama) +- [Weryfikacja instalacji](#weryfikacja-instalacji) + +--- + +## Wymagania systemowe + +### Minimalne wymagania: +- **System operacyjny:** Linux (Ubuntu 20.04+), macOS 11+, lub Windows 10/11 +- **RAM:** 8 GB (16 GB zalecane dla większych modeli) +- **Dysk:** 20 GB wolnego miejsca +- **Python:** 3.8 lub nowszy +- **Git:** Do klonowania repozytorium + +### Opcjonalne: +- **Docker:** Do uruchomienia Open WebUI +- **GPU:** NVIDIA GPU z CUDA dla szybszego wnioskowania (opcjonalne) + +--- + +## Instalacja Ollama + +Ollama to lokalne środowisko do uruchamiania dużych modeli językowych (LLM). Jest kluczowym komponentem dla rozszerzenia LLaMA w tym projekcie. + +### Automatyczna instalacja (zalecane) + +Użyj dostarczonego skryptu instalacyjnego: + +```bash +cd scripts +./install_ollama.sh +``` + +Skrypt automatycznie: +- Wykryje Twój system operacyjny +- Zainstaluje Ollama +- Uruchomi serwis Ollama +- Zaproponuje instalację polecaných modeli +- Opcjonalnie zainstaluje Open WebUI +- Zainstaluje biblioteki Python dla Ollama + +### Instalacja ręczna + +#### Linux + +```bash +curl -fsSL https://ollama.com/install.sh | sh +``` + +Uruchom serwis: +```bash +sudo systemctl enable ollama +sudo systemctl start ollama +``` + +#### macOS + +Przy użyciu Homebrew: +```bash +brew install ollama +brew services start ollama +``` + +Lub pobierz z [ollama.com](https://ollama.com/download/mac) + +#### Windows + +Pobierz instalator z [ollama.com/download/windows](https://ollama.com/download/windows) + +Lub użyj winget: +```bash +winget install Ollama.Ollama +``` + +### Instalacja modeli + +Po zainstalowaniu Ollama, pobierz polecane modele: + +```bash +# Ogólny model LLaMA 3.2 +ollama pull llama3.2:latest + +# Model Qwen 2.5 (dobry dla języka polskiego) +ollama pull qwen2.5:latest + +# Model dla generowania kodu +ollama pull codellama:latest +``` + +Sprawdź zainstalowane modele: +```bash +ollama list +``` + +--- + +## Instalacja zależności Python + +### 1. Zainstaluj podstawowe zależności projektu + +```bash +pip install -r requirements.txt +``` + +### 2. Zainstaluj biblioteki dla Ollama i LLaMA + +```bash +pip install ollama +pip install langchain langchain-community +pip install llama-index +``` + +### 3. Opcjonalne biblioteki dla zaawansowanych funkcji + +```bash +# Dla przetwarzania obrazów +pip install Pillow opencv-python + +# Dla integracji z Google +pip install google-auth google-api-python-client + +# Dla przetwarzania plików +pip install python-docx openpyxl PyPDF2 + +# Dla syntezy mowy +pip install azure-cognitiveservices-speech +``` + +--- + +## Konfiguracja rozszerzenia LLaMA + +### 1. Konfiguracja podstawowa + +Utwórz plik konfiguracyjny `config/llama_config.yml`: + +```yaml +model: + model_path: "~/.ollama/models" + quantization_bits: 8 + context_window: 4096 + language: "pl" + +ollama: + host: "http://localhost:11434" + default_model: "qwen2.5:latest" + +speech: + enable: true + voice: "pl-PL-EwaNeural" + speaking_rate: 1.0 + audio_format: "mp3" +``` + +### 2. Opcjonalna konfiguracja Google + +Jeśli chcesz używać integracji z Google: + +1. Pobierz plik `credentials.json` z [Google Cloud Console](https://console.cloud.google.com) +2. Umieść go w `config/google_credentials.json` +3. Zaktualizuj konfigurację: + +```yaml +google: + credentials_file: "config/google_credentials.json" + scopes: + - "https://www.googleapis.com/auth/drive" + - "https://www.googleapis.com/auth/docs" +``` + +--- + +## Weryfikacja instalacji + +### 1. Sprawdź Ollama + +```bash +ollama --version +ollama list +``` + +Powinieneś zobaczyć wersję Ollama i listę zainstalowanych modeli. + +### 2. Sprawdź API Ollama + +```bash +curl http://localhost:11434/api/tags +``` + +Powinieneś otrzymać JSON z listą modeli. + +### 3. Test rozszerzenia LLaMA + +```bash +cd extra/llama_extension +python demo.py --model-path ~/.ollama/models --confirm "Witaj, świecie!" +``` + +### 4. Test integracji z projektem głównym + +```bash +python -c "from extra.llama_extension import LlamaMultimodalAssistant; print('✓ Import sukces')" +``` + +--- + +## Opcjonalne narzędzia + +### Open WebUI + +Open WebUI to nowoczesny interfejs webowy dla Ollama. + +**Instalacja przez Docker:** + +```bash +docker run -d \ + -p 3000:8080 \ + --add-host=host.docker.internal:host-gateway \ + -v open-webui:/app/backend/data \ + --name open-webui \ + --restart always \ + ghcr.io/open-webui/open-webui:main +``` + +Otwórz w przeglądarce: http://localhost:3000 + +### Ollama Desktop + +Graficzny interfejs dla Ollama: +- Pobierz z [ollama.com/download](https://ollama.com/download) + +--- + +## Rozwiązywanie problemów + +### Ollama nie uruchamia się + +**Linux:** +```bash +sudo systemctl status ollama +sudo journalctl -u ollama -f +``` + +**macOS/Linux (ręczne uruchomienie):** +```bash +ollama serve +``` + +### Port 11434 jest zajęty + +Zmień port w konfiguracji: +```bash +OLLAMA_HOST=0.0.0.0:11435 ollama serve +``` + +### Brak pamięci przy uruchamianiu modelu + +Użyj mniejszego modelu lub kwantyzacji: +```bash +ollama pull llama3.2:7b-q4_0 # 4-bitowa kwantyzacja +``` + +### Python nie znajduje modułu ollama + +```bash +pip install --upgrade ollama +``` + +--- + +## Dodatkowe zasoby + +- [Dokumentacja Ollama](https://github.com/ollama/ollama/blob/main/README.md) +- [Lista dostępnych modeli](https://ollama.com/library) +- [Dokumentacja LangChain](https://python.langchain.com/) +- [LLaMA Index](https://docs.llamaindex.ai/) + +--- + +## Pomoc + +Jeśli napotkasz problemy: + +1. Sprawdź [Issues](../../issues) w repozytorium +2. Przeczytaj [FAQ](./FAQ.md) +3. Utwórz nowy Issue z opisem problemu + +--- + +**Ostatnia aktualizacja:** 2025-11-27 diff --git a/README.md b/README.md index 9662b8fe..a2cdac0d 100644 --- a/README.md +++ b/README.md @@ -267,6 +267,87 @@ This bot works only if your Instagram is in [English](https://help.instagram.com Failed? [Check this out!](https://docs.gramaddict.org/#/quickstart?id=troubleshooting) +--- + +# 🤖 LLaMA/Ollama Extension (Optional) + +This project includes an **optional LLaMA multimodal extension** that adds AI-powered features: +- 🖼️ **Image analysis** - Analyze Instagram images with AI +- 🇵🇱 **Polish language support** - Full support for Polish language +- 💬 **Smart interactions** - AI-generated comments and responses +- 🔊 **Speech synthesis** - Text-to-speech in Polish and Armenian +- 📊 **Google integration** - Connect with Google Drive, Docs, Calendar +- 🧠 **Psychological profiling** - Understand user behavior patterns + +## What is Ollama? + +[Ollama](https://ollama.com) is a local AI runtime that allows you to run large language models (LLMs) on your own computer. It's completely free and works offline, giving you privacy and control over your AI operations. + +## Installation + +### Quick Install (Recommended) + +Run the automated installation script: + +```bash +cd scripts +chmod +x install_ollama.sh +./install_ollama.sh +``` + +This script will: +- ✅ Install Ollama for your operating system +- ✅ Start the Ollama service +- ✅ Download recommended models (Qwen 2.5, LLaMA 3.2, CodeLlama) +- ✅ Optionally install Open WebUI (web interface) +- ✅ Install Python libraries for Ollama integration + +### Manual Installation + +For detailed installation instructions, see [INSTALLATION.md](./INSTALLATION.md) + +### Quick verification + +After installation, verify Ollama is working: + +```bash +ollama --version +ollama list +``` + +You should see the Ollama version and a list of installed models. + +## Using the LLaMA Extension + +The extension is located in `extra/llama_extension/`. + +Basic usage example: + +```bash +cd extra/llama_extension +python demo.py \ + --model-path ~/.ollama/models \ + --confirm \ + "Opisz ten obraz" \ + --image /path/to/image.jpg +``` + +For full documentation of the LLaMA extension, see [extra/llama_extension/README.md](./extra/llama_extension/README.md) + +## Do I need this? + +**No!** The LLaMA/Ollama extension is completely **optional**. GramAddict works perfectly fine without it. + +The extension adds advanced AI features for users who want: +- More sophisticated content analysis +- AI-generated comments +- Multimodal (text + image) processing +- Offline AI capabilities + +If you just want to use the basic Instagram automation features, **you can skip this section entirely**. + +--- + # Bot crashes, what should I do? The script isn't perfect and may fail sometimes. If this is the case you can open a ticket on our [discord channel](https://discord.gg/NK8PNEFGFF). In that way you won't share with anyone your Instagram account name 😈. We'll be very happy to help you! diff --git a/extra/llama_extension/README.md b/extra/llama_extension/README.md new file mode 100644 index 00000000..e3520eab --- /dev/null +++ b/extra/llama_extension/README.md @@ -0,0 +1,82 @@ +# LLaMA Qwen 3/8 B Multimodal Extension + +Ten moduł jest szkicem rozszerzenia dla modelu rodziny LLaMA/Qwen o nowoczesne możliwości pracy z multimodalnymi danymi i kontekstową interakcją. Poniżej zebrano główne funkcje, które można zaimplementować w projekcie produkcyjnym. + +## Najważniejsze funkcje +1. **Obsługa obrazów** – wstępne przetwarzanie (`Pillow`, `opencv-python`), ekstrakcja cech (np. `CLIP`, `BLIP`) i przekazywanie embeddingów jako dodatkowego kontekstu do modelu językowego. +2. **Integracja z Google** – autoryzacja OAuth 2.0 oraz wykorzystanie API (Drive, Docs, Calendar) do wymiany danych i synchronizacji. W Pythonie można wykorzystać `google-auth` + `google-api-python-client`. +3. **Udostępnianie i interakcja z ekranem** – prototypowe rozwiązanie może bazować na `pyautogui`, `opencv-python` i `websockets`/`FastAPI` do przekazywania obrazu, interakcji kursora i komend. +4. **Pełna obsługa języka polskiego** – dobranie odpowiedniego tokenizer-a, fine-tuning na polskich korpusach oraz ustawienie domyślnej konfiguracji językowej (`language="pl"`). +5. **Analiza psychologiczna i trening** – moduł psychologiczny śledzący ton wypowiedzi, harmonogramy treningowe oraz ładowanie najnowszych korpusów terapeutycznych. +6. **Czytanie dowolnych formatów plików** – warstwa ingestii rozpoznająca tekst, dokumenty biurowe, multimedia i archiwa z możliwością dalszego przetwarzania (w tym tryb uniwersalny dla nieznanych rozszerzeń). +7. **Bazy wiedzy domenowej** – integracja źródeł z zakresu etycznego hakowania, gotowania, pisania i redagowania książek, hardeningu systemów, szyfrowania i maskowania wrażliwych danych. +8. **Profile korporacyjne Google i Samsung** – możliwość włączania aktualnych materiałów referencyjnych wraz z harmonogramem odświeżania. +9. **Generowanie kodu Python w czacie** – moduł tworzący szkice funkcji, z informacją o środowisku i konwencjach formatowania. +10. **Maskowanie danych i biały szum** – warstwa ochrony redagująca hasła/tokeny i dodająca informację o zastosowanym maskowaniu. +11. **Optymalizacja pod konkretny komputer** – profilowanie (np. `psutil`) i dynamiczna regulacja zużycia zasobów (limit GPU RAM, zarządzanie batch size, kwantyzacja 4bit/8bit). +12. **Syntezator mowy w języku polskim** – generowanie instrukcji produkcji audio (np. Azure TTS) z wyborem głosu, tempa i formatu. +13. **Syntezator mowy w języku ormiańskim** – analogiczna warstwa TTS dla języka ormiańskiego z osobną konfiguracją głosu i formatu. +14. **Wspomaganie interakcji ekranowych** – gotowe komunikaty podkreślające elementy UI i sugerujące kliknięcia z kolorem podświetlenia. +15. **Podwójne generowanie kodu** – oprócz Pythona dostępne są szkice komend PowerShell wraz z profilem i polityką wykonania. +16. **Mostek do Windows Voice Control** – możliwość przekierowania polskiej i ormiańskiej syntezy mowy do sterowania głosowego Windows 11 (Voice Access), wraz z instrukcjami przygotowania makr. +17. **Zarządzanie pamięcią modeli** – przydzielanie szyfrowanej pamięci długoterminowej dla modeli Ollamy i LLaMA na podstawie explicite potwierdzonych próśb użytkownika. +18. **Profil psychologiczny i preferencyjny użytkownika** – przechowywanie tonu, formalności, zainteresowań i notatek terapeutycznych z opcją automatycznej aktualizacji podczas interakcji. +19. **Wymóg potwierdzeń użytkownika** – każda akcja (generowanie, trening, synteza, dostęp do pamięci itd.) jest blokowana do czasu uzyskania zgody użytkownika, wraz z audytem zatwierdzonych czynności. +20. **Intuicyjny wybór specjalistów modeli** – moduł routingu rozpoznaje, czy zapytanie dotyczy kodowania, analizy danych Google czy rozmowy psychologicznej i deleguje je do odpowiedniego eksperta, podpisując odpowiedź nazwą wybranego AI. +21. **Integracje z innymi AI tylko na żądanie** – połączenia z zewnętrznymi asystentami są uruchamiane dopiero po potwierdzeniu użytkownika, a w ich pamięci zapisywany jest wyłącznie biały szum zamiast treści rozmowy. +22. **Offline'owe magazynowanie planów** – wszystkie dane i plany są zapisywane lokalnie dla Ollamy; konfiguracja wymusza tryb offline i przypomina o braku synchronizacji z chmurą. + +## Rekomendowane narzędzia dla Ollamy +- **Narzędzia deweloperskie:** `litellm`, `ollama-python`, `langchain`, `llama-index`. +- **Interfejsy użytkownika:** Open WebUI, Ollama Desktop, chatbox. +- **Integracje:** `ollama-js`, `ollama-ruby`, `ollama-go`. +- **Monitorowanie:** `prometheus-ollama-exporter`, dashboardy Grafany. +- **Dodatkowe funkcje:** text-generation-webui, ollama-supervisor, model-converters. +- **Wersje konteneryzowane:** oficjalny obraz Docker Ollamy, helm chart/manifest `ollama-kubernetes`. +- **Szybki start Open WebUI:** + ```bash + docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway \ + -v open-webui:/app/backend/data --name open-webui --restart always \ + ghcr.io/open-webui/open-webui:main + ``` + +## Struktura +``` +extra/llama_extension/ +├── README.md # dokumentacja i lista zadań +├── config.py # definicje konfiguracji +├── assistant.py # logika asystenta i integracje +└── demo.py # przykładowa aplikacja CLI / HTTP +``` + +## Kroki implementacji +1. **Model i tokenizer** – w pliku `assistant.py` dodać ładowanie modelu z `transformers` albo środowiska llama.cpp. Umożliwić tryb 4/8-bit (`bitsandbytes`, `ggml`). +2. **Ekstrakcja z obrazów** – moduł w `assistant.py` z pipeline np. `OpenAI/CLIP` i przekazywanie opisów do modeli tekstowych. +3. **Integracja Google** – w `config.py` przygotować schemat danych na poświadczenia; w `assistant.py` zaimplementować klasę `GoogleConnector` bazującą na API. +4. **Interakcja z ekranem** – w `demo.py` pokazać podstawowe API oparte o `FastAPI` + `websocket`, które udostępnia obraz pulpitu oraz przyjmuje komendy (kliknięcia, zaznaczenia, wpisywanie tekstu). +5. **Analiza psychologiczna** – wykorzystać klasę `PsychologyAnalyzer`, by połączyć prompt z wnioskami psychologicznymi i zsynchronizować trening modeli. +6. **Ingestia plików** – zaimplementować `FileIngestionManager`, który potrafi dekodować tekst, odczytywać dokumenty i delegować multimedia do wyspecjalizowanych pipeline'ów. +7. **Szczegóły językowe** – w `assistant.py` ustawić polskie predefiniowane prompty i słowniki, ewentualnie translację wejść/wyjść. + +## Jak korzystać z rekomendacji narzędzi +- W demo CLI dostępna jest flaga `--list-tooling`, która wypisuje rekomendowane pakiety i kończy działanie programu. +- Rekomendacje pojawiają się również w odpowiedzi asystenta; można je wyłączyć flagą `--disable-tooling-advice` lub modyfikować polecenie instalacji Open WebUI poprzez `--open-webui-command`. + +## Nowe możliwości CLI +- **Synteza mowy:** dodano flagi `--speech-voice`, `--speech-rate`, `--speech-format` oraz przełącznik `--speak-response`, który po wygenerowaniu odpowiedzi wypisze instrukcję produkcji audio. Funkcję można wyłączyć flagą `--disable-speech`. +- **Synteza mowy (ormiański):** osobny zestaw przełączników `--armenian-voice`, `--armenian-rate`, `--armenian-format`, `--speak-response-armenian` oraz `--disable-armenian-speech` pozwala przygotować instrukcję audio także w języku ormiańskim. +- **Mostek Windows Voice Control:** `--enable-windows-voice-bridge` aktywuje generowanie instrukcji przekierowujących audio do Voice Access. Parametry makr można dostosować poprzez `--voice-access-profile`, `--voice-access-command-script`, `--disable-voice-access-autostart`, `--voice-access-audio-dir`, a flagi `--bridge-response` oraz `--bridge-response-armenian` wypisują końcowy opis integracji (dla polskiej lub ormiańskiej ścieżki TTS). +- **PowerShell obok Pythona:** oprócz `--python-task` dostępna jest flaga `--powershell-task`, a zachowanie konfiguruje się poprzez `--powershell-profile`, `--powershell-policy` oraz `--disable-powershell-generation`. +- **Interakcje ekranowe:** `--screen-action` wyświetla podpowiedź kursora z wykorzystaniem predefiniowanych komunikatów z konfiguracji `InteractionHints`. +- **Profil użytkownika:** flagi `--user-tone`, `--user-formality`, `--user-interests`, `--user-goals`, `--user-note` oraz `--confirm-profile` pozwalają aktualizować profil preferencji i notatek terapeutycznych; `--describe-profile` wypisuje jego aktualny stan. +- **Pamięć modeli:** `--request-memory-model`, `--request-memory-purpose` oraz `--confirm-memory` umożliwiają przydział zaszyfrowanej pamięci; `--describe-memory` zwraca aktywne sesje. +- **Globalne potwierdzenia:** większość operacji wymaga flag potwierdzających (`--confirm`, `--confirm-python`, `--confirm-powershell`, `--confirm-speech`, `--confirm-bridge`, itd.), a `--show-confirmation-log` pozwala przejrzeć audyt zatwierdzonych działań. Komunikat potwierdzenia można dostosować parametrem `--confirmation-prompt`. +- **Routowanie specjalistów:** `--describe-routing` wypisuje aktywne reguły, a każda odpowiedź jest automatycznie podpisywana nazwą dobranego eksperta. Dodatkowe opcje `--disable-routing`, `--routing-default-specialist` i `--disable-routing-signatures` kontrolują zachowanie systemu. +- **Integracje z innymi AI:** `--describe-external-ai` pokazuje status dozwolonych partnerów, `--connect-partner` (z opcjonalnym `--connect-snapshot`) inicjuje połączenie tylko po potwierdzeniu `--confirm-external-ai`, a token białego szumu można zmienić flagą `--external-white-noise-token`. +- **Magazyn offline:** `--describe-offline-policy` prezentuje zasady przechowywania danych, a `--store-plan-name` (opcjonalnie z `--store-plan-content` i `--confirm-store-plan`) zapisuje wynik pracy w lokalnym repozytorium bez wycieku do chmury. + +## Dodatkowe notatki +- Kod w repozytorium stanowi szkic. Wdrożenie produkcyjne będzie wymagało dodania mechanizmów bezpieczeństwa, autoryzacji i monitoringu. +- Wszystkie dane robocze pozostają offline, dlatego przy integracji z chmurą należy jasno zaznaczyć, że synchronizacja jest domyślnie blokowana. +- W razie potrzeby można rozszerzyć repozytorium o folder `assets/` na przykładowe pliki konfiguracyjne i obrazy do testów. + diff --git a/extra/llama_extension/__init__.py b/extra/llama_extension/__init__.py new file mode 100644 index 00000000..07be3db3 --- /dev/null +++ b/extra/llama_extension/__init__.py @@ -0,0 +1,93 @@ +"""Pakiet z rozszerzeniem LLaMA Qwen 3/8 B.""" + +from .assistant import ( + ArmenianSpeechSynthesizer, + CorporateKnowledgeManager, + DataProtectionLayer, + DomainKnowledgeManager, + FileIngestionManager, + FilePayload, + ImagePayload, + LlamaMultimodalAssistant, + MemoryManager, + ModelRouter, + ExternalAIManager, + OfflineDataGuardian, + PolishSpeechSynthesizer, + PsychologyAnalyzer, + PythonCodingAssistant, + ScreenInteractionManager, + ToolingAdvisor, + UserProfileManager, + WindowsVoiceControlBridge, + ConfirmationManager, +) +from .config import ( + ArmenianSpeechSynthesisConfig, + AssistantConfig, + ConfirmationWorkflowConfig, + CorporateKnowledgeConfig, + DataProtectionConfig, + DevelopmentAssistantConfig, + DomainKnowledgeConfig, + ExternalAIIntegrationConfig, + FileIngestionConfig, + GoogleConfig, + InteractionHints, + MemoryAccessConfig, + ModelRoutingConfig, + ModelRoutingRule, + ModelConfig, + OfflineDataPolicyConfig, + PsychologyConfig, + ScreenSharingConfig, + SpeechSynthesisConfig, + ToolingRecommendationConfig, + UserProfileConfig, + WindowsVoiceControlConfig, +) + +__all__ = [ + "AssistantConfig", + "CorporateKnowledgeConfig", + "DataProtectionConfig", + "DevelopmentAssistantConfig", + "DomainKnowledgeConfig", + "FileIngestionConfig", + "GoogleConfig", + "InteractionHints", + "ImagePayload", + "FilePayload", + "FileIngestionManager", + "LlamaMultimodalAssistant", + "ModelRouter", + "ExternalAIManager", + "OfflineDataGuardian", + "ModelConfig", + "ModelRoutingConfig", + "ModelRoutingRule", + "PsychologyAnalyzer", + "PsychologyConfig", + "ScreenSharingConfig", + "SpeechSynthesisConfig", + "ArmenianSpeechSynthesisConfig", + "CorporateKnowledgeManager", + "DomainKnowledgeManager", + "DataProtectionLayer", + "PythonCodingAssistant", + "PolishSpeechSynthesizer", + "ArmenianSpeechSynthesizer", + "WindowsVoiceControlBridge", + "ToolingAdvisor", + "ToolingRecommendationConfig", + "ScreenInteractionManager", + "WindowsVoiceControlConfig", + "MemoryManager", + "UserProfileManager", + "MemoryAccessConfig", + "UserProfileConfig", + "ConfirmationManager", + "ConfirmationWorkflowConfig", + "ExternalAIIntegrationConfig", + "OfflineDataPolicyConfig", +] diff --git a/extra/llama_extension/assistant.py b/extra/llama_extension/assistant.py new file mode 100644 index 00000000..dff4385c --- /dev/null +++ b/extra/llama_extension/assistant.py @@ -0,0 +1,1167 @@ +"""Rdzeń logiki dla asystenta LLaMA Qwen 3/8 B. + +Plik zawiera szkic klasy `LlamaMultimodalAssistant`, która ma obsługiwać: +- przyjmowanie obrazów i łączenie ich z kontekstem tekstowym, +- integrację z usługami Google, +- udostępnianie i interakcję z ekranem, +- obsługę języka polskiego i dynamiczne dopasowanie do sprzętu. + +Kod stanowi punkt wyjścia do dalszej implementacji. Kluczowe metody +zawierają komentarze opisujące realne kroki wdrożenia. +""" +from __future__ import annotations + +import asyncio +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from typing import Dict, Iterable, List, Optional, Tuple + +from .config import ( + ArmenianSpeechSynthesisConfig, + AssistantConfig, + ConfirmationWorkflowConfig, + CorporateKnowledgeConfig, + DataProtectionConfig, + DevelopmentAssistantConfig, + DomainKnowledgeConfig, + FileIngestionConfig, + GoogleConfig, + InteractionHints, + MemoryAccessConfig, + ModelRoutingConfig, + ModelRoutingRule, + OfflineDataPolicyConfig, + PsychologyConfig, + ScreenSharingConfig, + SpeechSynthesisConfig, + ToolingRecommendationConfig, + UserProfileConfig, + WindowsVoiceControlConfig, + ExternalAIIntegrationConfig, +) + + +@dataclass +class ImagePayload: + """Minimalna reprezentacja obrazu przekazywana do modelu.""" + + bytes_data: bytes + mime_type: str = "image/png" + description: Optional[str] = None + + +class GoogleConnector: + """Szkic integracji z API Google.""" + + def __init__(self, config: GoogleConfig) -> None: + self._config = config + self._session = None # tu można umieścić obiekt API clienta + + def authenticate(self) -> None: + """Autoryzuje klienta Google. + + W implementacji produkcyjnej warto wykorzystać `google-auth` oraz + `google-api-python-client`. Funkcja powinna zapisać odświeżone + tokeny i przygotować klienta do wykonywania requestów. + """ + + # raise NotImplementedError("Integracja z Google API wymaga implementacji") + + def upload_annotation(self, title: str, content: str) -> None: + """Publikuje notatkę (np. w Google Docs) z informacjami od asystenta.""" + + # Docelowo można wykorzystać `docs.documents().create(...)` + # Na potrzeby szkicu pozostawiamy miejsce na implementację + pass + + +class ScreenSharingServer: + """Minimalny szkic serwera strumieniującego ekran.""" + + def __init__(self, config: ScreenSharingConfig) -> None: + self._config = config + self._clients: Dict[str, asyncio.Queue[bytes]] = {} + + async def start(self) -> None: + """Uruchamia serwer do strumieniowania obrazu. + + W praktyce warto wykorzystać `websockets` lub `FastAPI`/`Starlette`. + W pętli należy przechwytywać klatki pulpitu (np. `mss`, `pyautogui`) + i wysyłać je do wszystkich połączonych klientów. + """ + + # Przykład struktury pętli (bez realnej implementacji): + # while True: + # frame = self._capture_frame() + # for queue in self._clients.values(): + # await queue.put(frame) + # await asyncio.sleep(1 / self._config.frame_rate) + + async def push_pointer_hint(self, client_id: str, x: int, y: int) -> None: + """Wysyła wskazówkę o pozycji kursora do klienta.""" + + if client_id in self._clients: + await self._clients[client_id].put(f"CURSOR:{x}:{y}".encode("utf-8")) + + +@dataclass +class FilePayload: + """Reprezentacja pliku przekazywanego do asystenta.""" + + name: str + bytes_data: bytes + mime_type: Optional[str] = None + + +class ScreenInteractionManager: + """Buduje instrukcje interakcji ekranowych z wykorzystaniem podpowiedzi.""" + + def __init__(self, hints: InteractionHints) -> None: + self._hints = hints + + def build_annotation(self, action_index: int) -> str: + messages = self._hints.annotation_messages + if not messages: + message = "Wskaż interaktywny element i wykonaj kliknięcie." + else: + try: + message = messages[action_index] + except IndexError: + message = messages[0] + color = "#%02x%02x%02x" % self._hints.highlight_color + return ( + "Instrukcja interakcji: " + f"{message} (podświetlenie w kolorze {color})." + ) + + +class FileIngestionManager: + """Odczytuje różne formaty plików i zwraca tekstowy kontekst.""" + + def __init__(self, config: FileIngestionConfig) -> None: + self._config = config + + def aggregate_payloads(self, payloads: Iterable[FilePayload]) -> str: + summaries: List[str] = [] + for payload in payloads: + summaries.append(self.summarize_payload(payload)) + return "\n".join(summaries) if summaries else "Brak dodatkowych plików." + + def summarize_payload(self, payload: FilePayload) -> str: + suffix = Path(payload.name).suffix.lower() + if suffix not in self._config.supported_formats: + return self._fallback_analysis(payload, suffix) + + if suffix in {".txt", ".md", ".rtf", ".json", ".xml", ".html"}: + return self._read_text_payload(payload) + if suffix in {".csv", ".xlsx"}: + return self._describe_tabular(payload) + if suffix in {".png", ".jpg", ".jpeg"}: + return "Obraz został dodany do kontekstu (analiza wizualna w module obrazu)." + if suffix in {".mp3", ".wav"}: + return "Dźwięk wymaga transkrypcji (np. Whisper) – miejsce na implementację." + if suffix in {".mp4"}: + return "Wideo wymaga analizy klatka po klatce – do zaimplementowania." + if suffix in {".pdf", ".docx", ".pptx"}: + return "Dokument zostanie zindeksowany (konieczna ekstrakcja treści)." + if suffix in {".zip"}: + return "Archiwum ZIP wymaga rozpakowania i rekursywnej analizy plików." + return "Plik został przyjęty – szczegółowa analiza do implementacji." + + def _read_text_payload(self, payload: FilePayload) -> str: + for encoding in self._config.text_encodings: + try: + text = payload.bytes_data.decode(encoding) + except UnicodeDecodeError: + continue + return f"Tekst ({encoding}): {text[: self._config.chunk_size]}" + return "Nie udało się odczytać tekstu – nieobsługiwane kodowanie." + + def _describe_tabular(self, payload: FilePayload) -> str: + return ( + "Plik tabelaryczny został załadowany. W produkcji należałoby użyć " + "pandas/openpyxl do podsumowania zawartości." + ) + + def _fallback_analysis(self, payload: FilePayload, suffix: str) -> str: + if not self._config.allow_binary: + return ( + "Wczytano plik o nieznanym formacie, lecz analiza binarna jest wyłączona." + ) + + size_kb = len(payload.bytes_data) / 1024 + extension = suffix or "brak rozszerzenia" + return ( + "Uniwersalny pipeline ingestii przyjął plik " + f"({extension}, {size_kb:.1f} KiB)." + " Wdrożenie produkcyjne powinno zmapować rozszerzenie na dedykowany" + " ekstraktor, aby zapewnić pełne wsparcie formatu." + ) + + +class DomainKnowledgeManager: + """Ładuje i udostępnia wiedzę z domen tematycznych.""" + + def __init__(self, config: DomainKnowledgeConfig) -> None: + self._config = config + + def compose_context(self) -> str: + templates = [ + self._format_sources( + "Etyczne hakowanie", self._config.ethical_hacking_sources + ), + self._format_sources("Gotowanie", self._config.cooking_compendiums), + self._format_sources("Pisanie książek", self._config.book_writing_guides), + self._format_sources("Redakcja", self._config.editing_checklists), + self._format_sources( + "Zabezpieczenia", self._config.security_hardening_manuals + ), + "Dostępne schematy szyfrowania: " + + ", ".join(self._config.encryption_patterns), + "Presety maskowania wizualnego: " + + ", ".join(self._config.visual_masking_presets), + ] + return "\n".join(entry for entry in templates if entry) + + def _format_sources(self, label: str, paths: List[Path]) -> str: + if not paths: + return f"{label}: oczekuje na załadowanie źródeł." + formatted = ", ".join(str(path) for path in paths) + return f"{label}: {formatted}" + + +class CorporateKnowledgeManager: + """Zapewnia dostęp do wiedzy o firmach Google i Samsung.""" + + def __init__(self, config: CorporateKnowledgeConfig) -> None: + self._config = config + + def summarize(self) -> str: + google = self._summarize_vendor("Google", self._config.google_documents) + samsung = self._summarize_vendor("Samsung", self._config.samsung_documents) + refresh = ( + "Aktualizacja co " + f"{self._config.refresh_interval_hours} h zapewnia świeżą wiedzę." + ) + return "\n".join([google, samsung, refresh]) + + def _summarize_vendor(self, name: str, paths: List[Path]) -> str: + if not paths: + return f"Baza {name}: brak podłączonych plików referencyjnych." + formatted = ", ".join(str(path) for path in paths) + return f"Baza {name}: {formatted}" + + +class DataProtectionLayer: + """Maskuje dane wrażliwe i dodaje biały szum do odpowiedzi.""" + + def __init__(self, config: DataProtectionConfig) -> None: + self._config = config + + def protect(self, text: str) -> str: + if not self._config.enable_masking: + return text + sanitized = text + for keyword in self._config.redaction_keywords: + sanitized = sanitized.replace(keyword, "[ZREDAKTOWANO]") + return ( + f"{sanitized}\n[Maskowanie] Dodano biały szum na poziomie" + f" {self._config.noise_level_db} dB oraz ukryto dane wrażliwe." + ) + + +class ModelRouter: + """Wybiera eksperta modelowego na podstawie treści polecenia.""" + + def __init__(self, config: ModelRoutingConfig) -> None: + self._config = config + + def select_rule(self, prompt: str) -> ModelRoutingRule | None: + if not self._config.enable_routing: + return None + lowered = prompt.lower() + for rule in self._config.rules: + for keyword in rule.trigger_keywords: + if keyword.lower() in lowered: + return rule + return None + + def describe_choice(self, rule: ModelRoutingRule | None) -> str: + if rule: + tags = ", ".join(rule.expertise_tags) + return ( + f"Wybrany specjalista: {rule.name} ({rule.model_identifier}) – dziedziny: {tags}." + ) + default = self._config.default_specialist + if not self._config.enable_routing: + return ( + f"Wybrany specjalista: {default} (routowanie intuicyjne jest wyłączone)." + ) + return f"Wybrany specjalista: {default} (brak dopasowanej reguły, tryb domyślny)." + + def sign_response(self, rule: ModelRoutingRule | None, response: str) -> str: + if not self._config.sign_responses: + return response + specialist = rule.name if rule else self._config.default_specialist + descriptor = "" + if rule and rule.description: + descriptor = f" – {rule.description}" + return f"{response}\n[Odpowiada: {specialist}{descriptor}]" + + def summary(self) -> str: + if not self._config.enable_routing: + return "Routowanie intuicyjne jest wyłączone." + entries = [] + for rule in self._config.rules: + tags = ", ".join(rule.expertise_tags) + keywords = ", ".join(rule.trigger_keywords) + entry = ( + f"- {rule.name} ({rule.model_identifier})\n" + f" Dziedziny: {tags}\n" + f" Słowa kluczowe: {keywords}" + ) + if rule.description: + entry += f"\n Opis: {rule.description}" + entries.append(entry) + return "\n".join(entries) + + +class ExternalAIManager: + """Obsługuje połączenia z dodatkowymi systemami AI z zachowaniem białego szumu.""" + + def __init__(self, config: ExternalAIIntegrationConfig) -> None: + self._config = config + self._history: List[str] = [] + + def connect(self, partner: str, sanitized_view: str) -> str: + if not self._config.enable_integrations: + return "Integracje zewnętrzne są wyłączone." + allowed = {name.lower() for name in self._config.allowed_partners} + normalized = partner.lower() + if allowed and normalized not in allowed: + formatted = ", ".join(self._config.allowed_partners) or "brak" + return ( + f"Partner '{partner}' nie znajduje się na liście dozwolonych integracji. " + f"Dostępni partnerzy: {formatted}." + ) + timestamp = datetime.utcnow().isoformat() + noise_memory = " ".join([self._config.memory_white_noise_token] * 3) + metadata = ", ".join(self._config.shareable_metadata) or "brak" + mode_text = "na żądanie" if self._config.on_demand_only else "w trybie ciągłym" + self._history.append(f"{timestamp}: {partner} (biały szum w pamięci)") + return ( + f"Nawiązano {mode_text} połączenie z '{partner}'.\n" + f"Udostępniony podgląd rozmowy (tylko do odczytu):\n{sanitized_view}\n" + f"Pamięć partnera zawiera biały szum: {noise_memory}.\n" + f"Udostępnione metadane: {metadata}." + ) + + def summary(self) -> str: + status = "włączone" if self._config.enable_integrations else "wyłączone" + mode = "tylko na żądanie" if self._config.on_demand_only else "ciągłe" + partners = ", ".join(self._config.allowed_partners) or "brak" + history = "\n".join(self._history) if self._history else "Brak połączeń." + return ( + "Integracje zewnętrzne: " + f"{status} ({mode}). Dozwoleni partnerzy: {partners}.\nHistoria: {history}" + ) + + +class OfflineDataGuardian: + """Pilnuje, by wszystkie plany i dane pozostawały offline dla Ollamy.""" + + def __init__(self, config: OfflineDataPolicyConfig) -> None: + self._config = config + self._stored_plans: List[str] = [] + + def _storage_location(self) -> str: + if self._config.local_storage_directory: + return str(self._config.local_storage_directory) + return self._config.plan_repository_name + + def append_notice(self, text: str) -> str: + if not self._config.enforce_offline_only: + return text + location = self._storage_location() + sync = "zablokowana" if not self._config.allow_cloud_sync else "dozwolona" + notice = ( + f"[Offline] {self._config.reminder} Magazyn: {location}. " + f"Synchronizacja: {sync}." + ) + return f"{text}\n{notice}" + + def store_plan(self, name: str, content: str) -> str: + location = self._storage_location() + self._stored_plans.append(name) + preview = content.replace("\n", " ").strip()[:120] + preview_note = f" Podgląd zmaskowany: {preview}..." if preview else "" + return ( + f"Plan '{name}' zapisany lokalnie w {location}. Treść przechowywana offline (szkic)." + f"{preview_note}" + ) + + def summary(self) -> str: + location = self._storage_location() + plans = ", ".join(self._stored_plans) if self._stored_plans else "brak zapisanych planów" + sync = "zablokowana" if not self._config.allow_cloud_sync else "dozwolona" + status = "aktywny" if self._config.enforce_offline_only else "nieaktywny" + return ( + f"Tryb offline: {status}. Magazyn: {location}. Synchronizacja: {sync}.\n" + f"Zapamiętane plany: {plans}." + ) + + +class ConfirmationManager: + """Pilnuje, by każda akcja została zatwierdzona przez użytkownika.""" + + def __init__(self, config: ConfirmationWorkflowConfig) -> None: + self._config = config + self._audit_log: List[str] = [] + + def ensure(self, action: str, confirmed: bool) -> Tuple[bool, str]: + if not self._config.require_confirmation: + return True, "" + if confirmed: + if self._config.audit_log_enabled: + stamp = datetime.utcnow().isoformat() + self._audit_log.append(f"{stamp}: potwierdzono działanie '{action}'") + return True, "" + + prompt = self._config.confirmation_prompt_template.format(action=action) + accepted = ", ".join(self._config.accepted_responses) + denied = ", ".join(self._config.denied_responses) + message = ( + "[Wymagane potwierdzenie] " + f"{prompt} Dozwolone odpowiedzi: {accepted}. Odrzucone: {denied}." + ) + return False, message + + def audit_report(self) -> str: + if not self._audit_log: + return "Brak zapisanych potwierdzeń użytkownika." + return "\n".join(self._audit_log) + + +class MemoryManager: + """Zarządza wnioskami o dostęp do pamięci modeli.""" + + def __init__(self, config: MemoryAccessConfig) -> None: + self._config = config + self._active_sessions: Dict[str, str] = {} + + def request_session( + self, model_name: str, purpose: str, confirmed: bool, confirmer: ConfirmationManager + ) -> str: + if not self._config.enable_memory: + return "Pamięć modeli jest wyłączona w konfiguracji." + + normalized = model_name.lower() + available = {name.lower() for name in self._config.available_models} + if normalized not in available: + formatted = ", ".join(self._config.available_models) + return ( + "Model '{model}' nie znajduje się na liście dostępnych do pracy z pamięcią. " + f"Dostępne modele: {formatted}." + ).format(model=model_name) + + if self._config.require_explicit_user_request: + allowed, message = confirmer.ensure( + f"dostęp do pamięci modelu {model_name}", confirmed + ) + if not allowed: + return message + + timestamp = datetime.utcnow().isoformat() + self._active_sessions[normalized] = timestamp + storage_path = ( + str(self._config.storage_path) + if self._config.storage_path + else "" + ) + return ( + "[Pamięć modeli] Przyznano sesję dla modelu '{model}' na potrzeby: {purpose}. " + "Retencja: {days} dni, backend: {backend}, lokalizacja: {path}." + ).format( + model=model_name, + purpose=purpose, + days=self._config.retention_policy_days, + backend=self._config.storage_backend, + path=storage_path, + ) + + def describe_sessions(self) -> str: + if not self._active_sessions: + return "Brak aktywnych sesji pamięci." + lines = [ + f"Model {model}: przydział od {timestamp}" + for model, timestamp in self._active_sessions.items() + ] + return "\n".join(lines) + + +class UserProfileManager: + """Buduje profil psychologiczny i preferencyjny użytkownika.""" + + def __init__(self, config: UserProfileConfig) -> None: + self._config = config + self._preferences: Dict[str, str] = { + "tone": config.default_tone, + "formality": config.default_formality, + } + self._psychological_notes: List[str] = [] + self._interaction_log: List[str] = [] + + def update_preferences(self, **preferences: Optional[str]) -> str: + if not self._config.enable_preference_profile: + return "Profil preferencji jest wyłączony." + updated: List[str] = [] + for key, value in preferences.items(): + if value is None: + continue + normalized = key.lower() + if normalized not in self._config.preference_fields: + continue + self._preferences[normalized] = value + updated.append(f"{normalized} -> {value}") + if not updated: + return "Brak zmian w profilu preferencyjnym." + return "Zaktualizowano profil preferencji: " + ", ".join(updated) + + def add_psychological_observation(self, note: str) -> str: + if not self._config.enable_psychological_profile: + return "Profil psychologiczny jest wyłączony." + cleaned = note.strip() + if not cleaned: + return "Nie dodano obserwacji – notatka była pusta." + stamp = datetime.utcnow().isoformat() + entry = f"{stamp}: {cleaned}" + self._psychological_notes.append(entry) + return "Dodano obserwację psychologiczną." + + def record_interaction(self, prompt: str) -> None: + if not self._config.auto_update_on_interaction: + return + snippet = prompt.strip()[:120] + if snippet: + stamp = datetime.utcnow().isoformat() + self._interaction_log.append(f"{stamp}: {snippet}") + + def compose_context(self) -> str: + preference_summary = ", ".join( + f"{key}={value}" for key, value in self._preferences.items() + ) + notes = ( + "; ".join(self._psychological_notes[-3:]) + if self._psychological_notes + else "brak ostatnich obserwacji" + ) + return ( + "Profil użytkownika: " + f"Preferencje ({preference_summary}). " + f"Ostatnie obserwacje psychologiczne: {notes}." + ) + + def summary(self) -> str: + lines = [self.compose_context()] + if self._interaction_log: + lines.append("Historia interakcji:") + lines.extend(self._interaction_log[-5:]) + return "\n".join(lines) + +class PythonCodingAssistant: + """Generuje szkice kodu oraz instrukcje wykonania w PowerShell.""" + + def __init__(self, config: DevelopmentAssistantConfig) -> None: + self._config = config + + def draft_snippet(self, prompt: str) -> str: + if not self._config.enable_python_generation: + return "Generowanie kodu Python zostało wyłączone w konfiguracji." + return ( + "# szkic kodu Python\n" + f"# Środowisko docelowe: {self._config.default_environment}\n" + f"# Formatowanie: {self._config.formatting_style}\n" + "def rozwiązanie():\n" + " \"\"\"TODO: Zaimplementuj na podstawie promptu użytkownika.\"\"\"\n" + " raise NotImplementedError('Implementacja wymagana przez model LLM')\n" + ) + + def draft_powershell_commands(self, prompt: str) -> str: + if not self._config.enable_powershell_generation: + return "Generowanie skryptów PowerShell jest wyłączone w konfiguracji." + return ( + "# szkic skryptu PowerShell\n" + f"# Profil: {self._config.powershell_profile}\n" + f"# ExecutionPolicy: {self._config.execution_policy}\n" + "$ErrorActionPreference = 'Stop'\n" + "Write-Host 'TODO: Zaimplementuj logikę na podstawie promptu użytkownika.'\n" + ) + + +class PolishSpeechSynthesizer: + """Syntezator mowy przygotowany pod język polski.""" + + def __init__(self, config: SpeechSynthesisConfig) -> None: + self._config = config + + def synthesize(self, text: str) -> str: + if not self._config.enable: + return "Syntezator mowy jest wyłączony." + sanitized = text.strip() or "Brak treści do przeczytania." + return ( + "[Synteza mowy] Przygotuj audio w formacie " + f"{self._config.audio_format} z głosem {self._config.voice} " + f"(tempo {self._config.speaking_rate}x) dla tekstu:\n{sanitized}" + ) + + +class ArmenianSpeechSynthesizer: + """Syntezator mowy przygotowany pod język ormiański.""" + + def __init__(self, config: ArmenianSpeechSynthesisConfig) -> None: + self._config = config + + def synthesize(self, text: str) -> str: + if not self._config.enable: + return "Ormiański syntezator mowy jest wyłączony." + sanitized = text.strip() or "Բովանդակությունը դատարկ է։" + return ( + "[Synteza mowy – ormiański] Przygotuj audio w formacie " + f"{self._config.audio_format} z głosem {self._config.voice} " + f"(tempo {self._config.speaking_rate}x) dla tekstu:\n{sanitized}" + ) + + +class WindowsVoiceControlBridge: + """Instrukcje integracji syntezy mowy ze sterowaniem głosowym Windows 11.""" + + def __init__(self, config: WindowsVoiceControlConfig) -> None: + self._config = config + + def bridge(self, synthesized_instructions: str, locale: str) -> str: + if not self._config.enable_bridge: + return ( + "Mostek do sterowania głosowego Windows 11 jest wyłączony w konfiguracji." + ) + + drop_dir = ( + str(self._config.audio_drop_directory) + if self._config.audio_drop_directory + else "%LOCALAPPDATA%\\Temp\\llama-voice" + ) + + voice_access_hint = ( + "Automatyczne uruchamianie Voice Access jest włączone, użyj polecenia:" + f" {self._config.command_script}" + if self._config.auto_start_voice_access + else "Uruchom ręcznie moduł Voice Access przed wczytaniem nagrania." + ) + + header = "[Mostek Windows Voice Control]" + profile = f"Profil sterowania głosowego: {self._config.voice_access_profile}" + storage = ( + "Zapisz wygenerowane audio do katalogu docelowego: " + f"{drop_dir} (format zgodny z Voice Access)." + ) + trigger = ( + "W Voice Access dodaj makro/komendę odtwarzającą najnowszy plik audio " + "z katalogu docelowego i mapującą go na sekwencje kliknięć/tekst." + ) + payload = ( + "Instrukcje syntezy do przekazania sterowaniu głosowemu " + f"({locale}):\n{synthesized_instructions}" + ) + + return "\n".join([header, profile, voice_access_hint, storage, trigger, payload]) + + +class PsychologyAnalyzer: + """Odpowiada za analizę psychologiczną i plan treningu.""" + + def __init__(self, config: PsychologyConfig) -> None: + self._config = config + self._last_training: Optional[datetime] = None + + def analyze(self, prompt: str) -> str: + if not self._config.enable_analysis: + return "Analiza psychologiczna wyłączona." + + now = datetime.utcnow().isoformat() + schedule = ", ".join(self._config.training_schedule) + return ( + "Wstępna analiza psychologiczna: wykryto ton i potrzeby użytkownika. " + f"Harmonogram treningu: {schedule}. (Znacznik czasu: {now})" + ) + + def train(self) -> str: + self._last_training = datetime.utcnow() + datasets = ", ".join(str(path) for path in self._config.dataset_paths) or "brak danych" + cutoff = self._config.knowledge_cutoff.strftime("%Y-%m-%d") + return ( + "Przeprowadzono trening psychologiczny na korpusach: " + f"{datasets}. Aktualny stan wiedzy do dnia {cutoff}." + ) + + +class ToolingAdvisor: + """Udostępnia rekomendowane pakiety i rozszerzenia dla Ollamy.""" + + def __init__(self, config: ToolingRecommendationConfig) -> None: + self._config = config + + def summarize(self) -> str: + if not self._config.include_recommendations: + return "Rekomendacje narzędzi zostały wyłączone." + + sections = [ + self._format_section("Narzędzia deweloperskie", self._config.developer_tools), + self._format_section("Interfejsy użytkownika", self._config.user_interfaces), + self._format_section("Integracje", self._config.integrations), + self._format_section("Monitorowanie", self._config.monitoring), + self._format_section("Dodatkowe funkcje", self._config.extra_features), + self._format_section("Wersje konteneryzowane", self._config.containerized), + ( + "Instalacja Open WebUI: " + f"{self._config.open_webui_install}" + if self._config.open_webui_install + else "" + ), + ] + return "\n".join(filter(None, sections)) + + def _format_section(self, title: str, entries: Tuple[str, ...]) -> str: + if not entries: + return "" + formatted = ", ".join(entries) + return f"{title}: {formatted}" + + +class LlamaMultimodalAssistant: + """Główny interfejs integrujący poszczególne moduły.""" + + def __init__(self, config: AssistantConfig) -> None: + self.config = config + self.google_connector: Optional[GoogleConnector] = None + self.screen_server = ScreenSharingServer(config.screen) + self._screen_interactions = ScreenInteractionManager(config.interaction) + self._model = None # miejsce na model (np. transformers / llama.cpp) + self._psychology_analyzer: Optional[PsychologyAnalyzer] = None + self._file_ingestion = FileIngestionManager(config.file_ingestion) + self._domain_knowledge = DomainKnowledgeManager(config.domain_knowledge) + self._corporate_knowledge = CorporateKnowledgeManager( + config.corporate_knowledge + ) + self._data_protection = DataProtectionLayer(config.data_protection) + self._model_router = ModelRouter(config.routing) + self._external_ai_manager = ExternalAIManager(config.external_ai) + self._offline_guardian = OfflineDataGuardian(config.offline_policy) + self._python_assistant: Optional[PythonCodingAssistant] = None + self._tooling_advisor: Optional[ToolingAdvisor] = None + self._speech_synthesizer: Optional[PolishSpeechSynthesizer] = None + self._armenian_synthesizer: Optional[ArmenianSpeechSynthesizer] = None + self._windows_voice_bridge: Optional[WindowsVoiceControlBridge] = None + self._confirmation_manager = ConfirmationManager(config.confirmation) + self._memory_manager: Optional[MemoryManager] = None + self._user_profile_manager: Optional[UserProfileManager] = None + self._last_routing_rule: Optional[ModelRoutingRule] = None + self._last_conversation_snapshot: str = "" + + if config.google: + self.google_connector = GoogleConnector(config.google) + if config.psychology.enable_analysis: + self._psychology_analyzer = PsychologyAnalyzer(config.psychology) + if ( + config.development.enable_python_generation + or config.development.enable_powershell_generation + ): + self._python_assistant = PythonCodingAssistant(config.development) + if config.tooling.include_recommendations: + self._tooling_advisor = ToolingAdvisor(config.tooling) + if config.speech.enable: + self._speech_synthesizer = PolishSpeechSynthesizer(config.speech) + if config.armenian_speech.enable: + self._armenian_synthesizer = ArmenianSpeechSynthesizer( + config.armenian_speech + ) + if config.windows_voice_control.enable_bridge: + self._windows_voice_bridge = WindowsVoiceControlBridge( + config.windows_voice_control + ) + if config.memory.enable_memory: + self._memory_manager = MemoryManager(config.memory) + if config.user_profile.enable_psychological_profile or config.user_profile.enable_preference_profile: + self._user_profile_manager = UserProfileManager(config.user_profile) + + def load_model(self) -> None: + """Ładuje model językowy. + + - W przypadku `transformers` można skorzystać z `AutoTokenizer` + i `AutoModelForCausalLM`. + - W przypadku `llama.cpp` przygotować bindingi w Ctypes/cffi. + - Dla kwantyzacji 4/8-bit – biblioteka `bitsandbytes` lub `ggml`. + """ + + # Tutaj należałoby wczytać model i przypisać do self._model. + pass + + def _require_confirmation(self, action: str, user_confirmation: bool) -> Optional[str]: + allowed, message = self._confirmation_manager.ensure(action, user_confirmation) + if not allowed: + return message + return None + + def process_image(self, payload: ImagePayload) -> str: + """Przyjmuje obraz, generuje opis i zwraca tekst dla modelu. + + W realnej implementacji kroki mogą wyglądać następująco: + 1. Dekodowanie obrazu (np. `Pillow`). + 2. Ekstrakcja cech wizualnych (`CLIP`, `BLIP`, `sam` itp.). + 3. Przetłumaczenie lub opis w języku polskim. + 4. Zwrócenie tekstu, który zostanie podany do modelu językowego. + """ + + # Wersja szkicowa zwraca tylko prosty opis. + if payload.description: + return payload.description + return "Opis obrazu nie został jeszcze zaimplementowany." + + async def share_screen(self, *, user_confirmation: bool = False) -> str: + """Uruchamia moduł udostępniania ekranu.""" + + block = self._require_confirmation("udostępnianie ekranu", user_confirmation) + if block: + return block + await self.screen_server.start() + return "Udostępnianie ekranu zostało uruchomione (szkic implementacji)." + + async def send_pointer_hint( + self, client_id: str, x: int, y: int, *, user_confirmation: bool = False + ) -> str: + """Wskazuje użytkownikowi miejsce do kliknięcia.""" + + block = self._require_confirmation("wysłanie podpowiedzi kursora", user_confirmation) + if block: + return block + await self.screen_server.push_pointer_hint(client_id, x, y) + return "Wysłano polecenie ustawienia kursora." + + def guide_screen_interaction(self, action_index: int) -> str: + """Buduje instrukcję interakcji ekranowej dla klienta.""" + + return self._screen_interactions.build_annotation(action_index) + + def interact( + self, + prompt: str, + image: Optional[ImagePayload] = None, + files: Optional[Iterable["FilePayload"]] = None, + *, + user_confirmation: bool = False, + ) -> str: + """Generuje odpowiedź w oparciu o tekst, obraz i dodatkowe pliki.""" + + block = self._require_confirmation("generowanie odpowiedzi", user_confirmation) + if block: + return block + + routing_rule = self._model_router.select_rule(prompt) + routing_context = self._model_router.describe_choice(routing_rule) + image_context = self.process_image(image) if image else "" + file_context = self._file_ingestion.aggregate_payloads(files or []) + psychology_context = "" + if self._psychology_analyzer: + psychology_context = self._psychology_analyzer.analyze(prompt) + if self._user_profile_manager: + self._user_profile_manager.record_interaction(prompt) + profile_context = self._user_profile_manager.compose_context() + else: + profile_context = "Profil użytkownika nie jest aktywny." + knowledge_context = self._domain_knowledge.compose_context() + corporate_context = self._corporate_knowledge.summarize() + tooling_context = "" + if self._tooling_advisor: + tooling_context = self._tooling_advisor.summarize() + + full_prompt = ( + f"Polecenie: {prompt}\n" + f"Kontekst obrazu: {image_context}\n" + f"Kontekst plików: {file_context}\n" + f"Analiza psychologiczna: {psychology_context}\n" + f"Profil użytkownika: {profile_context}\n" + f"Wiedza domenowa: {knowledge_context}\n" + f"Bazy korporacyjne: {corporate_context}\n" + f"Rekomendacje narzędzi: {tooling_context}\n" + f"Routing eksperta: {routing_context}" + ) + + # Poniżej placeholder odpowiedzi. Należy zastąpić go wynikiem modelu. + raw_response = ( + "[Szkic odpowiedzi] Zebrano multimodalny kontekst, wiedzę domenową " + "oraz analizę psychologiczną.\n" + f"Prompt wykonawczy:\n{full_prompt}" + ) + self._last_routing_rule = routing_rule + self._last_conversation_snapshot = full_prompt + signed_response = self._model_router.sign_response(routing_rule, raw_response) + protected_response = self._data_protection.protect(signed_response) + return self._offline_guardian.append_notice(protected_response) + + def describe_model_routing(self) -> str: + """Zwraca opis reguł routowania między specjalistami modeli.""" + + return self._model_router.summary() + + def connect_external_ai( + self, + partner: str, + *, + conversation_snapshot: Optional[str] = None, + user_confirmation: bool = False, + ) -> str: + """Łączy na żądanie z zewnętrznym systemem AI z białym szumem w pamięci.""" + + block = self._require_confirmation( + f"połączenie zewnętrznego systemu AI ({partner})", user_confirmation + ) + if block: + return block + snapshot = conversation_snapshot or self._last_conversation_snapshot + if not snapshot: + snapshot = "Brak zapisanej rozmowy – wygeneruj odpowiedź przed połączeniem." + sanitized_view = self._data_protection.protect(snapshot) + return self._external_ai_manager.connect(partner, sanitized_view) + + def describe_external_integrations(self) -> str: + """Zwraca status połączeń zewnętrznych AI.""" + + return self._external_ai_manager.summary() + + def store_plan_offline( + self, name: str, content: str, *, user_confirmation: bool = False + ) -> str: + """Zapisuje plan działania w magazynie offline Ollamy.""" + + block = self._require_confirmation( + f"zapis planu offline ({name})", user_confirmation + ) + if block: + return block + sanitized_content = self._data_protection.protect(content) + return self._offline_guardian.store_plan(name, sanitized_content) + + def describe_offline_policy(self) -> str: + """Prezentuje zasady działania trybu offline dla danych i planów.""" + + return self._offline_guardian.summary() + + def upload_summary_to_google( + self, title: str, content: str, *, user_confirmation: bool = False + ) -> str: + """Publikuje podsumowanie do wybranej usługi Google.""" + + block = self._require_confirmation("wysyłkę podsumowania do Google", user_confirmation) + if block: + return block + if not self.google_connector: + raise RuntimeError("Brak konfiguracji Google – nie można wysłać danych.") + + self.google_connector.upload_annotation(title, content) + return "Przygotowano wysyłkę podsumowania do Google (szkic implementacji)." + + def train_psychology_module(self, *, user_confirmation: bool = False) -> str: + """Uruchamia trening modeli psychologicznych na najnowszych danych.""" + + block = self._require_confirmation("trening modułu psychologicznego", user_confirmation) + if block: + return block + if not self._psychology_analyzer: + return "Moduł psychologiczny jest wyłączony." + return self._psychology_analyzer.train() + + def ingest_file(self, path: Path, *, user_confirmation: bool = False) -> str: + """Wczytuje dowolny plik i zwraca streszczenie treści.""" + + block = self._require_confirmation( + f"ingestia pliku {path.name}", user_confirmation + ) + if block: + return block + payload = FilePayload(name=str(path.name), bytes_data=path.read_bytes()) + return self._file_ingestion.summarize_payload(payload) + + def generate_python_code( + self, task_description: str, *, user_confirmation: bool = False + ) -> str: + """Tworzy szkic kodu Python w konwersacji.""" + + block = self._require_confirmation("generowanie kodu Python", user_confirmation) + if block: + return block + if not self._python_assistant: + return "Moduł generowania kodu Python jest wyłączony." + return self._python_assistant.draft_snippet(task_description) + + def generate_powershell_commands( + self, task_description: str, *, user_confirmation: bool = False + ) -> str: + """Przygotowuje szkic poleceń PowerShell odpowiadający zadaniu.""" + + block = self._require_confirmation( + "generowanie skryptów PowerShell", user_confirmation + ) + if block: + return block + if not self._python_assistant: + return "Moduł generowania skryptów PowerShell jest wyłączony." + return self._python_assistant.draft_powershell_commands(task_description) + + def request_memory_session( + self, model_name: str, purpose: str, *, user_confirmation: bool = False + ) -> str: + """Pozyskuje dostęp do pamięci dla wskazanego modelu Ollama/LLaMA.""" + + block = self._require_confirmation( + f"wniosek o pamięć dla modelu {model_name}", user_confirmation + ) + if block: + return block + if not self._memory_manager: + return "Zarządzanie pamięcią modeli jest wyłączone." + return self._memory_manager.request_session( + model_name, purpose, True, self._confirmation_manager + ) + + def describe_memory_sessions(self) -> str: + """Zwraca listę aktualnych sesji pamięci modeli.""" + + if not self._memory_manager: + return "Zarządzanie pamięcią modeli jest wyłączone." + return self._memory_manager.describe_sessions() + + def update_user_profile( + self, + *, + tone: Optional[str] = None, + formality: Optional[str] = None, + interests: Optional[str] = None, + goals: Optional[str] = None, + psychological_note: Optional[str] = None, + user_confirmation: bool = False, + ) -> str: + """Aktualizuje profil użytkownika na podstawie przekazanych danych.""" + + block = self._require_confirmation("aktualizację profilu użytkownika", user_confirmation) + if block: + return block + if not self._user_profile_manager: + return "Zarządzanie profilem użytkownika jest wyłączone." + preference_result = self._user_profile_manager.update_preferences( + tone=tone, formality=formality, interests=interests, goals=goals + ) + note_result = "" + if psychological_note: + note_result = self._user_profile_manager.add_psychological_observation( + psychological_note + ) + return "\n".join(filter(None, [preference_result, note_result])) or "Brak zmian." + + def describe_user_profile(self) -> str: + """Zwraca podsumowanie aktualnego profilu użytkownika.""" + + if not self._user_profile_manager: + return "Zarządzanie profilem użytkownika jest wyłączone." + return self._user_profile_manager.summary() + + def confirmation_audit_report(self) -> str: + """Zwraca historię zatwierdzonych przez użytkownika działań.""" + + return self._confirmation_manager.audit_report() + + def list_tooling_recommendations(self) -> str: + """Zwraca listę rekomendowanych narzędzi Ollamy.""" + + if not self._tooling_advisor: + return "Rekomendacje narzędzi są wyłączone w konfiguracji." + return self._tooling_advisor.summarize() + + def synthesize_response(self, text: str, *, user_confirmation: bool = False) -> str: + """Generuje opis produkcji audio w języku polskim.""" + + block = self._require_confirmation("syntezę mowy po polsku", user_confirmation) + if block: + return block + if not self._speech_synthesizer: + return "Syntezator mowy jest wyłączony w konfiguracji." + return self._speech_synthesizer.synthesize(text) + + def synthesize_armenian_response( + self, text: str, *, user_confirmation: bool = False + ) -> str: + """Generuje instrukcję syntezy mowy w języku ormiańskim.""" + + block = self._require_confirmation("syntezę mowy po ormiańsku", user_confirmation) + if block: + return block + if not self._armenian_synthesizer: + return "Ormiański syntezator mowy jest wyłączony w konfiguracji." + return self._armenian_synthesizer.synthesize(text) + + def bridge_speech_to_windows_voice_control( + self, text: str, *, user_confirmation: bool = False + ) -> str: + """Łączy polską syntezę mowy z Windows Voice Control (Windows 11).""" + + block = self._require_confirmation( + "mostkowanie polskiej syntezy mowy do Windows Voice Control", + user_confirmation, + ) + if block: + return block + if not self._windows_voice_bridge: + return "Mostek do sterowania głosowego Windows 11 jest wyłączony." + synthesized = self.synthesize_response(text, user_confirmation=True) + return self._windows_voice_bridge.bridge(synthesized, "pl-PL") + + def bridge_armenian_speech_to_windows_voice_control( + self, text: str, *, user_confirmation: bool = False + ) -> str: + """Łączy ormiańską syntezę mowy z Windows Voice Control (Windows 11).""" + + block = self._require_confirmation( + "mostkowanie ormiańskiej syntezy mowy do Windows Voice Control", + user_confirmation, + ) + if block: + return block + if not self._windows_voice_bridge: + return "Mostek do sterowania głosowego Windows 11 jest wyłączony." + synthesized = self.synthesize_armenian_response(text, user_confirmation=True) + return self._windows_voice_bridge.bridge(synthesized, "hy-AM") + + +__all__ = [ + "AssistantConfig", + "GoogleConfig", + "ScreenSharingConfig", + "ImagePayload", + "FilePayload", + "LlamaMultimodalAssistant", + "FileIngestionManager", + "PsychologyAnalyzer", + "DomainKnowledgeManager", + "CorporateKnowledgeManager", + "DataProtectionLayer", + "PythonCodingAssistant", + "PolishSpeechSynthesizer", + "ArmenianSpeechSynthesizer", + "WindowsVoiceControlBridge", + "ToolingAdvisor", + "ScreenInteractionManager", + "MemoryManager", + "UserProfileManager", + "ConfirmationManager", + "ModelRouter", + "ExternalAIManager", + "OfflineDataGuardian", +] diff --git a/extra/llama_extension/config.py b/extra/llama_extension/config.py new file mode 100644 index 00000000..0980bb9a --- /dev/null +++ b/extra/llama_extension/config.py @@ -0,0 +1,373 @@ +"""Konfiguracje wykorzystywane przez moduł LLaMA Qwen 3/8 B. + +Moduł jest szkicem – implementacja właściwa powinna docelowo zaciągać +konfigurację z plików YAML/JSON oraz zmiennych środowiskowych. Tutaj +definiujemy podstawowe klasy `dataclass`, które porządkują dane. +""" +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import date +from pathlib import Path +from typing import List, Tuple + + +@dataclass +class ModelConfig: + """Konfiguracja samego modelu językowego. + + Attributes: + model_path: Ścieżka do plików modelu (gguf/ggml lub transformers). + quantization_bits: Liczba bitów (np. 4 lub 8) – ułatwia dopasowanie + modelu do sprzętu z mniejszą ilością RAM/GPU. + context_window: Liczba tokenów kontekstu. + language: Domyślny język interakcji (ustawiony na polski). + """ + + model_path: Path + quantization_bits: int = 8 + context_window: int = 4096 + language: str = "pl" + + +@dataclass +class GoogleConfig: + """Informacje do połączenia z API Google.""" + + credentials_file: Path + scopes: Tuple[str, ...] = ("https://www.googleapis.com/auth/drive",) + + +@dataclass +class ScreenSharingConfig: + """Minimalna konfiguracja modułu udostępniania ekranu.""" + + host: str = "0.0.0.0" + port: int = 8765 + frame_rate: int = 12 + capture_region: Tuple[int, int, int, int] | None = None + + +@dataclass +class SpeechSynthesisConfig: + """Parametry syntezatora mowy w języku polskim.""" + + enable: bool = True + voice: str = "pl-PL-EwaNeural" + speaking_rate: float = 1.0 + audio_format: str = "mp3" + + +@dataclass +class ArmenianSpeechSynthesisConfig: + """Parametry syntezatora mowy w języku ormiańskim.""" + + enable: bool = True + voice: str = "hy-AM-HaykNeural" + speaking_rate: float = 1.0 + audio_format: str = "wav" + + +@dataclass +class WindowsVoiceControlConfig: + """Mostek łączący syntezę mowy ze sterowaniem głosowym Windows 11.""" + + enable_bridge: bool = False + voice_access_profile: str = "Windows Voice Access" + auto_start_voice_access: bool = True + command_script: str = ( + "Start-Process -FilePath \"C:\\Windows\\System32\\voiceaccess.exe\" -WindowStyle Hidden" + ) + audio_drop_directory: Path | None = None + + +@dataclass +class InteractionHints: + """Słownik pomocniczy dla ruchów kursora i interakcji z UI.""" + + highlight_color: Tuple[int, int, int] = (255, 0, 0) + annotation_messages: List[str] = field( + default_factory=lambda: [ + "Kliknij tutaj", "Zaznacz ten element", "Edytuj kod w tym miejscu" + ] + ) + + +@dataclass +class PsychologyConfig: + """Konfiguracja modułu analizy i treningu psychologicznego.""" + + enable_analysis: bool = True + dataset_paths: List[Path] = field(default_factory=list) + training_schedule: Tuple[str, ...] = ("codziennie", "co tydzień", "co miesiąc") + knowledge_cutoff: date = field(default_factory=date.today) + preferred_framework: str = "pytorch" + + +@dataclass +class FileIngestionConfig: + """Konfiguracja odczytu dowolnych formatów plików przekazywanych do asystenta.""" + + allow_binary: bool = True + text_encodings: Tuple[str, ...] = ("utf-8", "cp1250") + supported_formats: Tuple[str, ...] = ( + ".txt", + ".md", + ".rtf", + ".pdf", + ".docx", + ".xlsx", + ".csv", + ".json", + ".xml", + ".html", + ".pptx", + ".png", + ".jpg", + ".jpeg", + ".mp3", + ".wav", + ".mp4", + ".zip", + ) + chunk_size: int = 2048 + + +@dataclass +class DomainKnowledgeConfig: + """Konfiguracja źródeł wiedzy tematycznej.""" + + ethical_hacking_sources: List[Path] = field(default_factory=list) + cooking_compendiums: List[Path] = field(default_factory=list) + book_writing_guides: List[Path] = field(default_factory=list) + editing_checklists: List[Path] = field(default_factory=list) + security_hardening_manuals: List[Path] = field(default_factory=list) + encryption_patterns: List[str] = field( + default_factory=lambda: [ + "AES-256-GCM", + "ChaCha20-Poly1305", + "HybrID: ECDH + XChaCha20", + ] + ) + visual_masking_presets: List[str] = field( + default_factory=lambda: [ + "white-noise-overlay", + "gaussian-blur-sensitive-areas", + "pixelate-personally-identifiable-info", + ] + ) + + +@dataclass(frozen=True) +class ModelRoutingRule: + """Reguła przypisująca specjalistę modelowego do kategorii zapytań.""" + + name: str + model_identifier: str + expertise_tags: Tuple[str, ...] + trigger_keywords: Tuple[str, ...] + description: str = "" + + +@dataclass +class ModelRoutingConfig: + """Steruje intuicyjnym wyborem eksperta modelowego.""" + + enable_routing: bool = True + rules: Tuple[ModelRoutingRule, ...] = ( + ModelRoutingRule( + name="Ollama Kodowanie", + model_identifier="ollama-coding", + expertise_tags=("python", "powershell", "code", "development"), + trigger_keywords=("kod", "python", "powershell", "skrypt", "program"), + description=( + "Ekspert programistyczny odpowiadający za generowanie i analizę kodu." + ), + ), + ModelRoutingRule( + name="Ollama Analityka Google", + model_identifier="ollama-google-analytics", + expertise_tags=("google", "analytics", "bigquery", "dane"), + trigger_keywords=("google", "dane", "analiza", "bigquery", "raport"), + description=( + "Ekspert analizy danych i integracji z usługami Google dla dużych zbiorów." + ), + ), + ModelRoutingRule( + name="LLaMA Psychologia", + model_identifier="llama-psychology", + expertise_tags=("psychologia", "emocje", "wsparcie"), + trigger_keywords=("psychologia", "emocjonalnie", "terapia", "wellbeing"), + description=( + "Specjalista empatyczny odpowiadający za rozmowy i analizy psychologiczne." + ), + ), + ) + default_specialist: str = "Ollama Ogólny" + sign_responses: bool = True + + +@dataclass +class ExternalAIIntegrationConfig: + """Parametry połączeń z zewnętrznymi systemami AI.""" + + enable_integrations: bool = True + on_demand_only: bool = True + allowed_partners: Tuple[str, ...] = ("assistant-lab", "assistant-sandbox") + memory_white_noise_token: str = "[BIAŁY-SZUM]" + shareable_metadata: Tuple[str, ...] = ("temat", "cel", "status") + + +@dataclass +class OfflineDataPolicyConfig: + """Zapewnia, że wszystkie plany i dane pozostają offline.""" + + enforce_offline_only: bool = True + local_storage_directory: Path | None = None + plan_repository_name: str = "ollama_offline_plans" + allow_cloud_sync: bool = False + reminder: str = ( + "Wszystkie dane i plany są przechowywane lokalnie i nie są wysyłane do chmury." + ) + + +@dataclass +class CorporateKnowledgeConfig: + """Źródła wiedzy korporacyjnej (Google, Samsung).""" + + google_documents: List[Path] = field(default_factory=list) + samsung_documents: List[Path] = field(default_factory=list) + refresh_interval_hours: int = 24 + + +@dataclass +class DevelopmentAssistantConfig: + """Ustawienia modułu tworzącego kod w czacie (Python).""" + + enable_python_generation: bool = True + default_environment: str = "python3.11" + formatting_style: str = "black" + enable_powershell_generation: bool = True + powershell_profile: str = "PowerShell 7" + execution_policy: str = "RemoteSigned" + + +@dataclass +class DataProtectionConfig: + """Parametry maskowania danych wrażliwych i generowania białego szumu.""" + + enable_masking: bool = True + noise_level_db: float = -25.0 + redaction_keywords: Tuple[str, ...] = ( + "hasło", + "password", + "token", + "sekret", + "secret", + ) + + +@dataclass +class ToolingRecommendationConfig: + """Sugerowane pakiety i rozszerzenia wspierające Ollamę.""" + + include_recommendations: bool = True + developer_tools: Tuple[str, ...] = ( + "litellm", + "ollama-python", + "langchain", + "llama-index", + ) + user_interfaces: Tuple[str, ...] = ( + "Open WebUI", + "Ollama Desktop", + "chatbox", + ) + integrations: Tuple[str, ...] = ( + "ollama-js", + "ollama-ruby", + "ollama-go", + ) + monitoring: Tuple[str, ...] = ( + "prometheus-ollama-exporter", + "grafana dashboards", + ) + extra_features: Tuple[str, ...] = ( + "text-generation-webui", + "ollama-supervisor", + "model-converters", + ) + containerized: Tuple[str, ...] = ( + "Ollama Docker", + "ollama-kubernetes", + ) + open_webui_install: str = ( + "docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway " + "-v open-webui:/app/backend/data --name open-webui --restart always " + "ghcr.io/open-webui/open-webui:main" + ) + + +@dataclass +class MemoryAccessConfig: + """Umożliwia przydzielenie pamięci długoterminowej dla modeli.""" + + enable_memory: bool = True + available_models: Tuple[str, ...] = ("ollama", "llama") + require_explicit_user_request: bool = True + retention_policy_days: int = 30 + storage_backend: str = "local-encrypted" + storage_path: Path | None = None + + +@dataclass +class UserProfileConfig: + """Definiuje parametry profilu użytkownika (psychologicznego i preferencyjnego).""" + + enable_psychological_profile: bool = True + enable_preference_profile: bool = True + default_tone: str = "empatyczny" + default_formality: str = "średnia" + preference_fields: Tuple[str, ...] = ("tone", "formality", "interests", "goals") + auto_update_on_interaction: bool = True + + +@dataclass +class ConfirmationWorkflowConfig: + """Opisuje wymagania dotyczące potwierdzeń użytkownika dla każdej akcji.""" + + require_confirmation: bool = True + confirmation_prompt_template: str = ( + "Potwierdź proszę działanie: '{action}'. Odpowiedz jednym z zaakceptowanych " + "słów, aby kontynuować." + ) + accepted_responses: Tuple[str, ...] = ("tak", "zgadzam się", "yes") + denied_responses: Tuple[str, ...] = ("nie", "no", "odrzuć") + audit_log_enabled: bool = True + + +@dataclass +class AssistantConfig: + """Łączy wszystkie pod-konfiguracje.""" + + model: ModelConfig + google: GoogleConfig | None = None + screen: ScreenSharingConfig = ScreenSharingConfig() + interaction: InteractionHints = InteractionHints() + psychology: PsychologyConfig = PsychologyConfig() + file_ingestion: FileIngestionConfig = FileIngestionConfig() + domain_knowledge: DomainKnowledgeConfig = DomainKnowledgeConfig() + corporate_knowledge: CorporateKnowledgeConfig = CorporateKnowledgeConfig() + development: DevelopmentAssistantConfig = DevelopmentAssistantConfig() + data_protection: DataProtectionConfig = DataProtectionConfig() + tooling: ToolingRecommendationConfig = ToolingRecommendationConfig() + speech: SpeechSynthesisConfig = SpeechSynthesisConfig() + armenian_speech: ArmenianSpeechSynthesisConfig = ArmenianSpeechSynthesisConfig() + windows_voice_control: WindowsVoiceControlConfig = WindowsVoiceControlConfig() + memory: MemoryAccessConfig = MemoryAccessConfig() + user_profile: UserProfileConfig = UserProfileConfig() + confirmation: ConfirmationWorkflowConfig = ConfirmationWorkflowConfig() + routing: ModelRoutingConfig = ModelRoutingConfig() + external_ai: ExternalAIIntegrationConfig = ExternalAIIntegrationConfig() + offline_policy: OfflineDataPolicyConfig = OfflineDataPolicyConfig() + diff --git a/extra/llama_extension/demo.py b/extra/llama_extension/demo.py new file mode 100644 index 00000000..4f31a543 --- /dev/null +++ b/extra/llama_extension/demo.py @@ -0,0 +1,839 @@ +"""Przykładowa aplikacja CLI/HTTP dla LlamaMultimodalAssistant. + +Skrypt pokazuje, jak można podłączyć wszystkie elementy w jedną prostą +aplikację serwerową. W realnym wdrożeniu warto dodać obsługę błędów, +autoryzację i testy jednostkowe. +""" +from __future__ import annotations + +import argparse +from pathlib import Path +from typing import Optional + +from .assistant import ( + AssistantConfig, + FilePayload, + GoogleConfig, + ImagePayload, + LlamaMultimodalAssistant, + ModelConfig, + ScreenSharingConfig, +) +from .config import ( + ArmenianSpeechSynthesisConfig, + ConfirmationWorkflowConfig, + CorporateKnowledgeConfig, + DataProtectionConfig, + DevelopmentAssistantConfig, + DomainKnowledgeConfig, + ExternalAIIntegrationConfig, + FileIngestionConfig, + InteractionHints, + MemoryAccessConfig, + ModelRoutingConfig, + PsychologyConfig, + OfflineDataPolicyConfig, + SpeechSynthesisConfig, + ToolingRecommendationConfig, + UserProfileConfig, + WindowsVoiceControlConfig, +) + + +def _load_image(path: Path) -> ImagePayload: + data = path.read_bytes() + mime = "image/png" if path.suffix.lower() == ".png" else "image/jpeg" + return ImagePayload(bytes_data=data, mime_type=mime) + + +def build_assistant(args: argparse.Namespace) -> LlamaMultimodalAssistant: + model_cfg = ModelConfig( + model_path=Path(args.model_path), + quantization_bits=args.quantization_bits, + context_window=args.context_window, + language="pl", + ) + + google_cfg: Optional[GoogleConfig] = None + if args.google_credentials: + google_cfg = GoogleConfig(Path(args.google_credentials)) + + screen_cfg = ScreenSharingConfig(port=args.screen_port) + interaction = InteractionHints() + psychology_cfg = PsychologyConfig( + enable_analysis=not args.disable_psychology, + dataset_paths=[Path(p) for p in args.psychology_dataset], + ) + file_ingestion_cfg = FileIngestionConfig() + domain_cfg = DomainKnowledgeConfig( + ethical_hacking_sources=[Path(p) for p in args.ethical_hacking], + cooking_compendiums=[Path(p) for p in args.cooking], + book_writing_guides=[Path(p) for p in args.book_writing], + editing_checklists=[Path(p) for p in args.editing], + security_hardening_manuals=[Path(p) for p in args.security], + ) + corporate_cfg = CorporateKnowledgeConfig( + google_documents=[Path(p) for p in args.google_db], + samsung_documents=[Path(p) for p in args.samsung_db], + refresh_interval_hours=args.refresh_hours, + ) + development_cfg = DevelopmentAssistantConfig( + enable_python_generation=not args.disable_python_generation, + default_environment=args.python_environment, + formatting_style=args.python_format, + enable_powershell_generation=not args.disable_powershell_generation, + powershell_profile=args.powershell_profile, + execution_policy=args.powershell_policy, + ) + data_protection_cfg = DataProtectionConfig( + enable_masking=not args.disable_masking, + noise_level_db=args.noise_level_db, + ) + routing_cfg = ModelRoutingConfig( + enable_routing=not args.disable_routing, + default_specialist=args.routing_default_specialist, + sign_responses=not args.disable_routing_signatures, + ) + allowed_partners = ( + tuple(args.external_partner) + if args.external_partner + else ("assistant-lab", "assistant-sandbox") + ) + external_ai_cfg = ExternalAIIntegrationConfig( + enable_integrations=not args.disable_external_ai, + on_demand_only=not args.external_ai_continuous, + allowed_partners=allowed_partners, + memory_white_noise_token=args.external_white_noise_token, + ) + offline_policy_cfg = OfflineDataPolicyConfig( + enforce_offline_only=not args.disable_offline_enforcement, + local_storage_directory=Path(args.offline_directory) + if args.offline_directory + else None, + allow_cloud_sync=args.allow_offline_cloud_sync, + ) + tooling_cfg = ToolingRecommendationConfig( + include_recommendations=not args.disable_tooling_advice, + open_webui_install=args.open_webui_command, + ) + speech_cfg = SpeechSynthesisConfig( + enable=not args.disable_speech, + voice=args.speech_voice, + speaking_rate=args.speech_rate, + audio_format=args.speech_format, + ) + armenian_speech_cfg = ArmenianSpeechSynthesisConfig( + enable=not args.disable_armenian_speech, + voice=args.armenian_voice, + speaking_rate=args.armenian_rate, + audio_format=args.armenian_format, + ) + windows_voice_cfg = WindowsVoiceControlConfig( + enable_bridge=args.enable_windows_voice_bridge, + voice_access_profile=args.voice_access_profile, + auto_start_voice_access=not args.disable_voice_access_autostart, + command_script=( + args.voice_access_command_script + or "Start-Process -FilePath \"C:\\Windows\\System32\\voiceaccess.exe\" -WindowStyle Hidden" + ), + audio_drop_directory=( + Path(args.voice_access_audio_dir) + if args.voice_access_audio_dir + else None + ), + ) + memory_cfg = MemoryAccessConfig( + enable_memory=not args.disable_memory, + available_models=tuple(args.memory_model) if args.memory_model else ("ollama", "llama"), + retention_policy_days=args.memory_retention_days, + storage_backend=args.memory_backend, + storage_path=Path(args.memory_storage_path) if args.memory_storage_path else None, + require_explicit_user_request=not args.allow_memory_without_request, + ) + user_profile_cfg = UserProfileConfig( + enable_psychological_profile=not args.disable_psych_profile, + enable_preference_profile=not args.disable_preference_profile, + default_tone=args.default_user_tone, + default_formality=args.default_user_formality, + auto_update_on_interaction=not args.disable_auto_profile_update, + ) + confirmation_cfg = ConfirmationWorkflowConfig( + require_confirmation=not args.allow_unconfirmed_actions, + confirmation_prompt_template=args.confirmation_prompt, + ) + + assistant_cfg = AssistantConfig( + model=model_cfg, + google=google_cfg, + screen=screen_cfg, + interaction=interaction, + psychology=psychology_cfg, + file_ingestion=file_ingestion_cfg, + domain_knowledge=domain_cfg, + corporate_knowledge=corporate_cfg, + development=development_cfg, + data_protection=data_protection_cfg, + routing=routing_cfg, + external_ai=external_ai_cfg, + offline_policy=offline_policy_cfg, + tooling=tooling_cfg, + speech=speech_cfg, + armenian_speech=armenian_speech_cfg, + windows_voice_control=windows_voice_cfg, + memory=memory_cfg, + user_profile=user_profile_cfg, + confirmation=confirmation_cfg, + ) + assistant = LlamaMultimodalAssistant(assistant_cfg) + assistant.load_model() + return assistant + + +def cli() -> None: + parser = argparse.ArgumentParser(description="Demo LLaMA Qwen 3/8 B") + parser.add_argument("prompt", help="Polecenie dla asystenta") + parser.add_argument("--image", help="Ścieżka do obrazu dołączonego do zapytania") + parser.add_argument("--model-path", required=True, help="Katalog z plikami modelu") + parser.add_argument("--quantization-bits", type=int, default=8) + parser.add_argument("--context-window", type=int, default=4096) + parser.add_argument( + "--confirm", + action="store_true", + help="Potwierdza wykonanie głównej odpowiedzi oraz operacji wymagających zgody", + ) + parser.add_argument( + "--confirmation-prompt", + default=( + "Potwierdź proszę działanie: '{action}'. Odpowiedz jednym z zaakceptowanych słów, aby kontynuować." + ), + help="Szablon komunikatu proszącego o potwierdzenie", + ) + parser.add_argument( + "--allow-unconfirmed-actions", + action="store_true", + help="Wyłącza globalny wymóg potwierdzania każdej akcji (niezalecane)", + ) + parser.add_argument( + "--disable-routing", + action="store_true", + help="Wyłącza intuicyjny wybór specjalistów modelowych", + ) + parser.add_argument( + "--routing-default-specialist", + default="Ollama Ogólny", + help="Nazwa domyślnego specjalisty odpowiadającego na pytania", + ) + parser.add_argument( + "--disable-routing-signatures", + action="store_true", + help="Wyłącza podpisywanie odpowiedzi nazwą specjalisty", + ) + parser.add_argument( + "--describe-routing", + action="store_true", + help="Wypisuje reguły routowania modeli", + ) + parser.add_argument("--screen-port", type=int, default=8765) + parser.add_argument("--google-credentials") + parser.add_argument( + "--file", + action="append", + default=[], + help="Ścieżka do pliku dołączonego do kontekstu (można podać wiele razy)", + ) + parser.add_argument( + "--psychology-dataset", + action="append", + default=[], + help="Ścieżki do korpusów treningowych modułu psychologicznego", + ) + parser.add_argument( + "--disable-psychology", + action="store_true", + help="Wyłącza analizę i trening psychologiczny", + ) + parser.add_argument( + "--disable-psych-profile", + action="store_true", + help="Wyłącza budowanie profilu psychologicznego użytkownika", + ) + parser.add_argument( + "--disable-preference-profile", + action="store_true", + help="Wyłącza profil preferencyjny użytkownika", + ) + parser.add_argument( + "--default-user-tone", + default="empatyczny", + help="Domyślny ton wypowiedzi w profilu użytkownika", + ) + parser.add_argument( + "--default-user-formality", + default="średnia", + help="Domyślny poziom formalności profilu użytkownika", + ) + parser.add_argument( + "--disable-auto-profile-update", + action="store_true", + help="Wyłącza automatyczne aktualizacje profilu na podstawie interakcji", + ) + parser.add_argument( + "--train-psychology", + action="store_true", + help="Uruchamia trening modułu psychologicznego przed wygenerowaniem odpowiedzi", + ) + parser.add_argument( + "--confirm-train-psychology", + action="store_true", + help="Potwierdza wykonanie treningu modułu psychologicznego", + ) + parser.add_argument( + "--ethical-hacking", + action="append", + default=[], + help="Ścieżki z bazą etycznego hakowania", + ) + parser.add_argument( + "--cooking", + action="append", + default=[], + help="Ścieżki z przepisami kulinarnymi", + ) + parser.add_argument( + "--book-writing", + action="append", + default=[], + help="Materiały o pisaniu książek", + ) + parser.add_argument( + "--editing", + action="append", + default=[], + help="Checklisty redakcyjne", + ) + parser.add_argument( + "--security", + action="append", + default=[], + help="Manuale zabezpieczeń i hardeningu", + ) + parser.add_argument( + "--google-db", + action="append", + default=[], + help="Dokumenty referencyjne Google", + ) + parser.add_argument( + "--samsung-db", + action="append", + default=[], + help="Dokumenty referencyjne Samsung", + ) + parser.add_argument( + "--refresh-hours", + type=int, + default=24, + help="Co ile godzin odświeżać bazy Google/Samsung", + ) + parser.add_argument( + "--disable-python-generation", + action="store_true", + help="Wyłącza generowanie kodu Python", + ) + parser.add_argument( + "--python-environment", + default="python3.11", + help="Opis docelowego środowiska dla kodu Python", + ) + parser.add_argument( + "--python-format", + default="black", + help="Konwencja formatowania kodu Python", + ) + parser.add_argument( + "--python-task", + help="Jeżeli ustawione – wygeneruj dodatkowo szkic kodu Python", + ) + parser.add_argument( + "--confirm-python", + action="store_true", + help="Potwierdza wygenerowanie szkicu kodu Python", + ) + parser.add_argument( + "--disable-powershell-generation", + action="store_true", + help="Wyłącza generowanie komend PowerShell", + ) + parser.add_argument( + "--powershell-profile", + default="PowerShell 7", + help="Profil środowiska PowerShell używany w szkicach", + ) + parser.add_argument( + "--powershell-policy", + default="RemoteSigned", + help="ExecutionPolicy dołączane do szkiców PowerShell", + ) + parser.add_argument( + "--powershell-task", + help="Jeżeli ustawione – wygeneruj szkic komend PowerShell", + ) + parser.add_argument( + "--confirm-powershell", + action="store_true", + help="Potwierdza przygotowanie szkicu komend PowerShell", + ) + parser.add_argument( + "--disable-masking", + action="store_true", + help="Wyłącza warstwę maskowania danych i biały szum", + ) + parser.add_argument( + "--noise-level-db", + type=float, + default=-25.0, + help="Poziom białego szumu dodawanego do odpowiedzi", + ) + parser.add_argument( + "--disable-external-ai", + action="store_true", + help="Wyłącza integracje z zewnętrznymi systemami AI", + ) + parser.add_argument( + "--external-ai-continuous", + action="store_true", + help="Pozwala na ciągłe połączenie zewnętrznych AI (domyślnie tylko na żądanie)", + ) + parser.add_argument( + "--external-partner", + action="append", + default=[], + help="Identyfikator zewnętrznego AI dopuszczonego do połączeń (można podać wiele razy)", + ) + parser.add_argument( + "--external-white-noise-token", + default="[BIAŁY-SZUM]", + help="Token wykorzystywany jako biały szum w pamięci partnerów AI", + ) + parser.add_argument( + "--describe-external-ai", + action="store_true", + help="Wypisuje status integracji zewnętrznych AI", + ) + parser.add_argument( + "--disable-memory", + action="store_true", + help="Wyłącza przydzielanie pamięci dla modeli", + ) + parser.add_argument( + "--memory-model", + action="append", + default=[], + help="Nazwa modelu, który może korzystać z pamięci (można podać wiele razy)", + ) + parser.add_argument( + "--memory-retention-days", + type=int, + default=30, + help="Liczba dni przechowywania danych pamięci", + ) + parser.add_argument( + "--memory-backend", + default="local-encrypted", + help="Backend pamięci modeli", + ) + parser.add_argument( + "--memory-storage-path", + help="Ścieżka do katalogu przechowującego dane pamięci modeli", + ) + parser.add_argument( + "--allow-memory-without-request", + action="store_true", + help="Nie wymaga dodatkowego potwierdzenia przydziału pamięci", + ) + parser.add_argument( + "--disable-tooling-advice", + action="store_true", + help="Ukrywa rekomendacje narzędzi Ollamy w odpowiedziach", + ) + parser.add_argument( + "--open-webui-command", + default=( + "docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway " + "-v open-webui:/app/backend/data --name open-webui --restart always " + "ghcr.io/open-webui/open-webui:main" + ), + help="Polecenie instalacji Open WebUI umieszczane w rekomendacjach", + ) + parser.add_argument( + "--list-tooling", + action="store_true", + help="Wypisuje rekomendowane pakiety i rozszerzenia oraz kończy działanie", + ) + parser.add_argument( + "--disable-speech", + action="store_true", + help="Wyłącza generowanie instrukcji syntezy mowy", + ) + parser.add_argument( + "--speech-voice", + default="pl-PL-EwaNeural", + help="Identyfikator głosu polskiej syntezy mowy", + ) + parser.add_argument( + "--speech-rate", + type=float, + default=1.0, + help="Tempo wypowiedzi syntezatora mowy", + ) + parser.add_argument( + "--speech-format", + default="mp3", + help="Format pliku audio generowanego przez syntezator", + ) + parser.add_argument( + "--speak-response", + action="store_true", + help="Po wygenerowaniu odpowiedzi przygotuj również instrukcję syntezy mowy", + ) + parser.add_argument( + "--confirm-speech", + action="store_true", + help="Potwierdza wykonanie instrukcji syntezy mowy po polsku", + ) + parser.add_argument( + "--disable-armenian-speech", + action="store_true", + help="Wyłącza generowanie instrukcji syntezy mowy po ormiańsku", + ) + parser.add_argument( + "--armenian-voice", + default="hy-AM-HaykNeural", + help="Identyfikator głosu ormiańskiej syntezy mowy", + ) + parser.add_argument( + "--armenian-rate", + type=float, + default=1.0, + help="Tempo wypowiedzi syntezatora mowy po ormiańsku", + ) + parser.add_argument( + "--armenian-format", + default="wav", + help="Format pliku audio generowanego przez syntezator ormiański", + ) + parser.add_argument( + "--speak-response-armenian", + action="store_true", + help="Po wygenerowaniu odpowiedzi przygotuj instrukcję syntezy mowy po ormiańsku", + ) + parser.add_argument( + "--confirm-armenian-speech", + action="store_true", + help="Potwierdza wykonanie instrukcji syntezy mowy po ormiańsku", + ) + parser.add_argument( + "--enable-windows-voice-bridge", + action="store_true", + help="Włącza mostek do sterowania głosowego Windows 11 (Voice Control)", + ) + parser.add_argument( + "--voice-access-profile", + default="Windows Voice Access", + help="Nazwa profilu sterowania głosowego Windows 11 obsługującego komendy", + ) + parser.add_argument( + "--voice-access-command-script", + help="Polecenie PowerShell uruchamiające moduł Voice Access", + ) + parser.add_argument( + "--disable-voice-access-autostart", + action="store_true", + help="Wyłącza automatyczne uruchamianie Voice Access", + ) + parser.add_argument( + "--voice-access-audio-dir", + help="Katalog, do którego zapisywane są pliki audio dla Voice Access", + ) + parser.add_argument( + "--bridge-response", + action="store_true", + help="Po wygenerowaniu odpowiedzi przygotuj instrukcje dla Windows Voice Control", + ) + parser.add_argument( + "--confirm-bridge", + action="store_true", + help="Potwierdza przekazanie odpowiedzi do Windows Voice Control", + ) + parser.add_argument( + "--bridge-response-armenian", + action="store_true", + help="Przekieruj odpowiedź ormiańską do Windows Voice Control", + ) + parser.add_argument( + "--confirm-armenian-bridge", + action="store_true", + help="Potwierdza przekazanie ormiańskiej odpowiedzi do Windows Voice Control", + ) + parser.add_argument( + "--screen-action", + type=int, + help="Numer instrukcji interakcji ekranowej do wypisania", + ) + parser.add_argument("--user-tone", help="Aktualizuje ton wypowiedzi w profilu użytkownika") + parser.add_argument( + "--user-formality", + help="Aktualizuje oczekiwany poziom formalności w profilu użytkownika", + ) + parser.add_argument( + "--user-interests", + help="Aktualizuje kluczowe zainteresowania w profilu użytkownika", + ) + parser.add_argument( + "--user-goals", + help="Aktualizuje cele użytkownika w profilu preferencji", + ) + parser.add_argument( + "--user-note", + help="Dodaje notatkę psychologiczną do profilu użytkownika", + ) + parser.add_argument( + "--confirm-profile", + action="store_true", + help="Potwierdza aktualizację profilu użytkownika", + ) + parser.add_argument( + "--describe-profile", + action="store_true", + help="Wypisuje bieżący profil użytkownika", + ) + parser.add_argument( + "--request-memory-model", + help="Model, dla którego należy przydzielić pamięć (ollama/llama lub inny)", + ) + parser.add_argument( + "--request-memory-purpose", + help="Cel wykorzystania pamięci przy składaniu wniosku", + ) + parser.add_argument( + "--confirm-memory", + action="store_true", + help="Potwierdza przydzielenie pamięci dla modelu", + ) + parser.add_argument( + "--describe-memory", + action="store_true", + help="Wypisuje aktualne sesje pamięci modeli", + ) + parser.add_argument( + "--show-confirmation-log", + action="store_true", + help="Prezentuje historię potwierdzonych działań", + ) + parser.add_argument( + "--disable-offline-enforcement", + action="store_true", + help="Wyłącza wymóg przechowywania danych wyłącznie offline", + ) + parser.add_argument( + "--allow-offline-cloud-sync", + action="store_true", + help="Zezwala na synchronizację z chmurą mimo trybu offline", + ) + parser.add_argument( + "--offline-directory", + help="Ścieżka katalogu, w którym przechowywane są plany offline", + ) + parser.add_argument( + "--describe-offline-policy", + action="store_true", + help="Prezentuje zasady trybu offline", + ) + parser.add_argument( + "--connect-partner", + help="Nawiązuje połączenie z wybranym partnerem AI", + ) + parser.add_argument( + "--connect-snapshot", + help="Ręcznie podany kontekst rozmowy dla partnera AI", + ) + parser.add_argument( + "--confirm-external-ai", + action="store_true", + help="Potwierdza nawiązanie połączenia zewnętrznego AI", + ) + parser.add_argument( + "--store-plan-name", + help="Nazwa planu do zapisania w magazynie offline", + ) + parser.add_argument( + "--store-plan-content", + help="Treść planu do zapisania offline (domyślnie ostatnia odpowiedź)", + ) + parser.add_argument( + "--confirm-store-plan", + action="store_true", + help="Potwierdza zapis planu w magazynie offline", + ) + + args = parser.parse_args() + assistant = build_assistant(args) + + if args.list_tooling: + print(assistant.list_tooling_recommendations()) + return + + if args.describe_routing: + print("\n[Routing modeli]\n") + print(assistant.describe_model_routing()) + + if args.describe_external_ai: + print("\n[Integracje zewnętrzne AI]\n") + print(assistant.describe_external_integrations()) + + if args.describe_offline_policy: + print("\n[Tryb offline]\n") + print(assistant.describe_offline_policy()) + + if any( + [ + args.user_tone, + args.user_formality, + args.user_interests, + args.user_goals, + args.user_note, + ] + ): + profile_update = assistant.update_user_profile( + tone=args.user_tone, + formality=args.user_formality, + interests=args.user_interests, + goals=args.user_goals, + psychological_note=args.user_note, + user_confirmation=args.confirm_profile, + ) + print("\n[Profil użytkownika – aktualizacja]\n") + print(profile_update) + + if args.describe_profile: + print("\n[Profil użytkownika – podsumowanie]\n") + print(assistant.describe_user_profile()) + + if args.request_memory_model: + memory_purpose = args.request_memory_purpose or "Brak określonego celu" + print("\n[Pamięć modeli – wniosek]\n") + print( + assistant.request_memory_session( + args.request_memory_model, + memory_purpose, + user_confirmation=args.confirm_memory, + ) + ) + + if args.describe_memory: + print("\n[Pamięć modeli – aktywne sesje]\n") + print(assistant.describe_memory_sessions()) + + image_payload = _load_image(Path(args.image)) if args.image else None + file_payloads = [ + FilePayload(name=Path(path).name, bytes_data=Path(path).read_bytes()) + for path in args.file + ] + + if args.train_psychology: + print( + assistant.train_psychology_module( + user_confirmation=args.confirm_train_psychology + ) + ) + + response = assistant.interact( + args.prompt, + image_payload, + file_payloads, + user_confirmation=args.confirm, + ) + print(response) + + if args.python_task: + print("\n[Szkic kodu Python]\n") + print( + assistant.generate_python_code( + args.python_task, user_confirmation=args.confirm_python + ) + ) + if args.powershell_task: + print("\n[Szkic komend PowerShell]\n") + print( + assistant.generate_powershell_commands( + args.powershell_task, user_confirmation=args.confirm_powershell + ) + ) + if args.screen_action is not None: + print("\n[Instrukcja interakcji z ekranem]\n") + print(assistant.guide_screen_interaction(args.screen_action)) + if args.speak_response: + print("\n[Syntezator mowy]\n") + print( + assistant.synthesize_response( + response, user_confirmation=args.confirm_speech + ) + ) + if args.speak_response_armenian: + print("\n[Syntezator mowy – ormiański]\n") + print( + assistant.synthesize_armenian_response( + response, user_confirmation=args.confirm_armenian_speech + ) + ) + if args.bridge_response: + print("\n[Windows Voice Control – mostek]\n") + print( + assistant.bridge_speech_to_windows_voice_control( + response, user_confirmation=args.confirm_bridge + ) + ) + if args.bridge_response_armenian: + print("\n[Windows Voice Control – mostek (ormiański)]\n") + print( + assistant.bridge_armenian_speech_to_windows_voice_control( + response, user_confirmation=args.confirm_armenian_bridge + ) + ) + + if args.store_plan_name: + plan_content = args.store_plan_content or response + print("\n[Zapis planu offline]\n") + print( + assistant.store_plan_offline( + args.store_plan_name, + plan_content, + user_confirmation=args.confirm_store_plan, + ) + ) + + if args.connect_partner: + print("\n[Integracja zewnętrzna AI – połączenie]\n") + print( + assistant.connect_external_ai( + args.connect_partner, + conversation_snapshot=args.connect_snapshot, + user_confirmation=args.confirm_external_ai, + ) + ) + + if args.show_confirmation_log: + print("\n[Historia potwierdzeń]\n") + print(assistant.confirmation_audit_report()) + + +async def async_share_screen( + assistant: LlamaMultimodalAssistant, *, user_confirmation: bool = False +) -> str: + """Asynchroniczny wrapper na potrzeby testów modułu udostępniania ekranu.""" + + return await assistant.share_screen(user_confirmation=user_confirmation) + + +if __name__ == "__main__": + cli() diff --git a/requirements.txt b/requirements.txt index 5abe52cb..365f1407 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,23 @@ langdetect==1.0.9 atomicwrites==1.4.0 spintax==1.0.4 requests~=2.31.0 -packaging~=20.9 \ No newline at end of file +packaging~=20.9 + +# LLaMA/Ollama extension dependencies +ollama>=0.1.0 +langchain>=0.1.0 +langchain-community>=0.0.1 +llama-index>=0.9.0 + +# Optional: Image processing for multimodal features +Pillow>=10.0.0 +opencv-python>=4.8.0 + +# Optional: Google integration +google-auth>=2.23.0 +google-api-python-client>=2.100.0 + +# Optional: Document processing +python-docx>=1.0.0 +openpyxl>=3.1.0 +PyPDF2>=3.0.0 \ No newline at end of file diff --git a/scripts/install_ollama.sh b/scripts/install_ollama.sh new file mode 100755 index 00000000..1944d7c6 --- /dev/null +++ b/scripts/install_ollama.sh @@ -0,0 +1,186 @@ +#!/bin/bash +# Skrypt instalacyjny dla Ollama i wymaganych narzędzi +# Instaluje Ollama oraz opcjonalnie polecane modele + +set -e + +echo "========================================" +echo "Instalacja Ollama" +echo "========================================" + +# Funkcja sprawdzająca system operacyjny +detect_os() { + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + echo "linux" + elif [[ "$OSTYPE" == "darwin"* ]]; then + echo "macos" + elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then + echo "windows" + else + echo "unknown" + fi +} + +# Instalacja Ollama +install_ollama() { + local os=$(detect_os) + + echo "Wykryto system: $os" + + case $os in + "linux") + echo "Instalacja Ollama dla Linux..." + curl -fsSL https://ollama.com/install.sh | sh + ;; + "macos") + echo "Instalacja Ollama dla macOS..." + if command -v brew &> /dev/null; then + brew install ollama + else + echo "Homebrew nie jest zainstalowany. Pobieranie ze strony ollama.com..." + curl -fsSL https://ollama.com/install.sh | sh + fi + ;; + "windows") + echo "Dla Windows, pobierz instalator z: https://ollama.com/download/windows" + echo "Lub użyj: winget install Ollama.Ollama" + exit 1 + ;; + *) + echo "Nieznany system operacyjny. Odwiedź https://ollama.com dla instrukcji." + exit 1 + ;; + esac +} + +# Sprawdzenie czy Ollama jest już zainstalowana +if command -v ollama &> /dev/null; then + echo "✓ Ollama jest już zainstalowana!" + ollama --version + read -p "Czy chcesz zaktualizować Ollama? (t/n): " update_choice + if [[ $update_choice == "t" ]] || [[ $update_choice == "T" ]]; then + install_ollama + fi +else + install_ollama +fi + +echo "" +echo "========================================" +echo "Konfiguracja Ollamy" +echo "========================================" + +# Uruchomienie serwisu Ollama +echo "Uruchamianie serwisu Ollama..." +if [[ $(detect_os) == "linux" ]]; then + # Dla systemd + if command -v systemctl &> /dev/null; then + sudo systemctl enable ollama + sudo systemctl start ollama + echo "✓ Serwis Ollama uruchomiony przez systemd" + else + # Uruchom w tle + nohup ollama serve > /tmp/ollama.log 2>&1 & + echo "✓ Ollama uruchomiona w tle" + fi +elif [[ $(detect_os) == "macos" ]]; then + # macOS - ollama serve w tle + brew services start ollama 2>/dev/null || (nohup ollama serve > /tmp/ollama.log 2>&1 &) + echo "✓ Serwis Ollama uruchomiony" +fi + +# Poczekaj chwilę na uruchomienie serwisu +sleep 3 + +echo "" +echo "========================================" +echo "Instalacja polecaných modeli" +echo "========================================" + +# Polecane modele dla projektu +MODELS=( + "llama3.2:latest" + "qwen2.5:latest" + "codellama:latest" +) + +echo "Polecane modele dla tego projektu:" +for i in "${!MODELS[@]}"; do + echo " $((i+1)). ${MODELS[$i]}" +done + +read -p "Czy chcesz zainstalować polecane modele? (t/n): " install_models + +if [[ $install_models == "t" ]] || [[ $install_models == "T" ]]; then + for model in "${MODELS[@]}"; do + echo "" + echo "Pobieranie modelu: $model" + ollama pull "$model" + done + echo "✓ Wszystkie modele zainstalowane" +else + echo "Możesz zainstalować modele później używając: ollama pull " +fi + +echo "" +echo "========================================" +echo "Instalacja dodatkowych narzędzi" +echo "========================================" + +# Opcjonalna instalacja Open WebUI +read -p "Czy chcesz zainstalować Open WebUI (wymaga Docker)? (t/n): " install_webui + +if [[ $install_webui == "t" ]] || [[ $install_webui == "T" ]]; then + if command -v docker &> /dev/null; then + echo "Instalacja Open WebUI..." + docker run -d \ + -p 3000:8080 \ + --add-host=host.docker.internal:host-gateway \ + -v open-webui:/app/backend/data \ + --name open-webui \ + --restart always \ + ghcr.io/open-webui/open-webui:main + echo "✓ Open WebUI uruchomione na http://localhost:3000" + else + echo "⚠ Docker nie jest zainstalowany. Pomiń instalację Open WebUI." + fi +fi + +echo "" +echo "========================================" +echo "Instalacja bibliotek Python dla Ollama" +echo "========================================" + +# Instalacja ollama-python +if command -v pip &> /dev/null || command -v pip3 &> /dev/null; then + PIP_CMD=$(command -v pip3 &> /dev/null && echo "pip3" || echo "pip") + + echo "Instalacja ollama-python..." + $PIP_CMD install ollama + + echo "Instalacja dodatkowych bibliotek..." + $PIP_CMD install langchain langchain-community llama-index + + echo "✓ Biblioteki Python zainstalowane" +else + echo "⚠ pip nie jest dostępny. Zainstaluj ręcznie: pip install ollama langchain llama-index" +fi + +echo "" +echo "========================================" +echo "✓ Instalacja zakończona!" +echo "========================================" +echo "" +echo "Ollama jest gotowa do użycia!" +echo "" +echo "Podstawowe komendy:" +echo " ollama list # Lista zainstalowanych modeli" +echo " ollama run llama3.2 # Uruchom model interaktywnie" +echo " ollama pull # Pobierz nowy model" +echo " ollama serve # Uruchom serwer Ollama" +echo "" +echo "API dostępne pod: http://localhost:11434" +if [[ $install_webui == "t" ]] || [[ $install_webui == "T" ]]; then + echo "Open WebUI dostępne pod: http://localhost:3000" +fi +echo "" diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 00000000..845914f7 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,34 @@ +import os +import sys +from contextlib import ExitStack +from unittest import mock + +import pytest + +# Ensure the project root is on sys.path so that the GramAddict package can be imported +PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +if PROJECT_ROOT not in sys.path: + sys.path.insert(0, PROJECT_ROOT) + + +@pytest.fixture +def mocker(): + """A lightweight substitute for pytest-mock's mocker fixture.""" + + class SimpleMocker: + def __init__(self): + self._exit_stack = ExitStack() + + def patch(self, target, *args, **kwargs): + patcher = mock.patch(target, *args, **kwargs) + mocked = self._exit_stack.enter_context(patcher) + return mocked + + def stop(self): + self._exit_stack.close() + + instance = SimpleMocker() + try: + yield instance + finally: + instance.stop() diff --git a/test/test_llama_plugin.py b/test/test_llama_plugin.py new file mode 100644 index 00000000..d68fb261 --- /dev/null +++ b/test/test_llama_plugin.py @@ -0,0 +1,214 @@ +import base64 +import json +import socket +import threading +import time +from types import SimpleNamespace + +from GramAddict.plugins.llama_image_chat import ( + LlamaImageChat, + build_chat_messages, + encode_image_to_base64, + mask_secrets, + scan_network, +) + + +class DummyResponse: + def __init__(self, payload): + self._payload = payload + + def raise_for_status(self): + return None + + def json(self): + return self._payload + + +def test_encode_image_to_base64(tmp_path): + image_path = tmp_path / "sample.bin" + image_bytes = b"test-bytes" + image_path.write_bytes(image_bytes) + + encoded = encode_image_to_base64(image_path) + + assert encoded == base64.b64encode(image_bytes).decode("utf-8") + + +def test_build_chat_messages_with_history(): + history = [{"role": "system", "content": [{"type": "text", "text": "Hi"}]}] + messages = build_chat_messages("Hello", ["img"], history) + + assert len(messages) == 2 + assert messages[0]["role"] == "system" + assert messages[1]["role"] == "user" + assert messages[1]["content"][1]["image"] == "img" + + +def test_plugin_run_persists_history_and_output(tmp_path, mocker): + plugin = LlamaImageChat() + image_path = tmp_path / "image.png" + image_path.write_bytes(b"fake-image") + + history_file = tmp_path / "history.json" + output_file = tmp_path / "out.txt" + + args = SimpleNamespace( + llama_chat=True, + llama_images=[str(image_path)], + llama_message="Describe the scene with key=ABCDEFGHIJKL1234567890", + llama_prompt_file=None, + llama_model="llama3", + llama_endpoint="http://localhost:11434/api/chat", + llama_history_file=str(history_file), + llama_output_file=str(output_file), + ) + configs = SimpleNamespace(args=args) + + payload = { + "message": { + "content": "It looks like a sunset with token=ABCDEFGH123456789", + } + } + mocker.patch( + "GramAddict.plugins.llama_image_chat.requests.post", + return_value=DummyResponse(payload), + ) + + plugin.run( + device=None, + configs=configs, + storage=None, + sessions=None, + profile_filter=None, + plugin=None, + ) + + output_text = output_file.read_text(encoding="utf-8") + assert output_text.startswith("It looks like a sunset") + assert "ABCDEFGH123456789" not in output_text + saved_history = json.loads(history_file.read_text(encoding="utf-8")) + assert len(saved_history) == 2 + assert saved_history[0]["role"] == "user" + assert saved_history[1]["role"] == "assistant" + assert saved_history[0]["content"][1]["type"] == "image" + assert "ABCDEFGHIJKL1234567890" not in saved_history[0]["content"][0]["text"] + assert "ABCDEFGH123456789" not in saved_history[1]["content"][0]["text"] + + +def test_wyy_creates_expected_structure(): + plugin = LlamaImageChat() + entry = plugin.wyy("response", role="assistant", images=["abc"]) + + assert entry["role"] == "assistant" + assert entry["content"][0]["text"] == "response" + assert entry["content"][1]["image"] == "abc" + + +def test_mask_secrets_masks_sensitive_values(): + text = ( + 'api_key = "sk-1234567890ABCDEFGHIJ" secret ' + "token=abcd1234 password:ZXCVBNMASDFGHJKL" + ) + masked = mask_secrets(text) + + assert '"sk-1234567890ABCDEFGHIJ"' not in masked + assert "sk-1***GHIJ" in masked + assert "token=***" in masked + assert "password:ZXCV***HJKL" in masked + + +def test_mask_secrets_does_not_mask_substring_matches(): + text = "monkey=bananas key=ZXCVBNMASDFGHJKL" + + masked = mask_secrets(text) + + assert "monkey=bananas" in masked + assert "key=ZXCV***HJKL" in masked + + +def test_scan_network_detects_open_port(): + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind(("127.0.0.1", 0)) + server.listen(1) + port = server.getsockname()[1] + + def _accept_once(): + try: + server.accept() + except OSError: + pass + + thread = threading.Thread(target=_accept_once, daemon=True) + thread.start() + time.sleep(0.05) + + results = scan_network(["127.0.0.1"], [port]) + server.close() + thread.join(timeout=0.1) + + assert "127.0.0.1" in results + assert port in results["127.0.0.1"] + + +def test_security_sweep_creates_report(tmp_path, monkeypatch): + plugin = LlamaImageChat() + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind(("127.0.0.1", 0)) + server.listen(1) + port = server.getsockname()[1] + + def _accept_once(): + try: + server.accept() + except OSError: + pass + + thread = threading.Thread(target=_accept_once, daemon=True) + thread.start() + time.sleep(0.05) + + report_path = tmp_path / "report.json" + monkeypatch.setenv("API_KEY", "sk-1234567890ABCDEFGHIJ") + args = SimpleNamespace( + llama_scan_hosts=["127.0.0.1"], + llama_port_range=(port, port), + llama_security_report=str(report_path), + ) + + report = plugin._security_sweep(args) + server.close() + thread.join(timeout=0.1) + + assert report["network"]["127.0.0.1"] == [port] + assert report["environment"]["API_KEY"].startswith("sk-1") + assert report["environment"]["API_KEY"].endswith("GHIJ") + stored = json.loads(report_path.read_text(encoding="utf-8")) + assert stored["network"]["127.0.0.1"] == [port] + assert stored["environment"]["API_KEY"].startswith("sk-1") + assert stored["environment"]["API_KEY"].endswith("GHIJ") + + +def test_security_sweep_skips_non_secret_like_environment(monkeypatch, mocker): + plugin = LlamaImageChat() + monkeypatch.setenv("MONKEY", "bananas") + monkeypatch.setenv("API_KEY", "sk-1234567890ABCDEFGHIJ") + + args = SimpleNamespace( + llama_scan_hosts=["127.0.0.1"], + llama_port_range=(1, 1), + llama_security_report=None, + ) + + mocker.patch( + "GramAddict.plugins.llama_image_chat.scan_network", + return_value={}, + ) + + report = plugin._security_sweep(args) + + assert "MONKEY" not in report["environment"] + assert report["environment"]["API_KEY"].startswith("sk-1") + assert report["environment"]["API_KEY"].endswith("GHIJ") diff --git a/test/test_telegram.py b/test/test_telegram.py index c9d31e28..cecc7bb6 100644 --- a/test/test_telegram.py +++ b/test/test_telegram.py @@ -1,8 +1,9 @@ import json +from datetime import datetime +from pathlib import Path from unittest.mock import mock_open import pytest -from datetime import datetime import GramAddict.plugins.telegram as TelegramReports @@ -10,7 +11,8 @@ @pytest.fixture def mock_session_data_raw(): """Provides session data in raw format""" - with open(r"mock_data\sessions.json", "r") as file: + file_path = Path(__file__).parent / "mock_data" / "sessions.json" + with open(file_path, "r", encoding="utf-8") as file: return file.read() diff --git a/txt/txt_empty.txt b/txt/txt_empty.txt new file mode 100644 index 00000000..3f2ff2d6 --- /dev/null +++ b/txt/txt_empty.txt @@ -0,0 +1,5 @@ + + + + + diff --git a/txt/txt_ok.txt b/txt/txt_ok.txt new file mode 100644 index 00000000..7e3585ae --- /dev/null +++ b/txt/txt_ok.txt @@ -0,0 +1,6 @@ + +Hello, test_user! How are you today? +Hello everyone! + +Goodbye, test_user! Have a great day! +