From 6a6fbb78a084ddbe4020bc49f8ad73e8516c3e82 Mon Sep 17 00:00:00 2001 From: Karentonoyan Date: Fri, 24 Oct 2025 11:56:13 +0200 Subject: [PATCH 1/8] Improve Telegram session loading resiliency --- "\"b/mock_data\\\\sessions.json\"" | 142 +++++++++++++++++++++++++++++ GramAddict/plugins/telegram.py | 11 ++- test/conftest.py | 34 +++++++ txt/txt_empty.txt | 5 + txt/txt_ok.txt | 6 ++ 5 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 "\"b/mock_data\\\\sessions.json\"" create mode 100644 test/conftest.py create mode 100644 txt/txt_empty.txt create mode 100644 txt/txt_ok.txt 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/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/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/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! + From c0d1bbcf088131bc761ab6744aed134896099f47 Mon Sep 17 00:00:00 2001 From: Karentonoyan Date: Sat, 1 Nov 2025 01:02:38 +0100 Subject: [PATCH 2/8] Enhance LLaMA plugin with security safeguards --- GramAddict/plugins/llama_image_chat.py | 396 +++++++++++++++++++++++++ test/test_llama_plugin.py | 171 +++++++++++ test/test_telegram.py | 6 +- 3 files changed, 571 insertions(+), 2 deletions(-) create mode 100644 GramAddict/plugins/llama_image_chat.py create mode 100644 test/test_llama_plugin.py diff --git a/GramAddict/plugins/llama_image_chat.py b/GramAddict/plugins/llama_image_chat.py new file mode 100644 index 00000000..d8d26b91 --- /dev/null +++ b/GramAddict/plugins/llama_image_chat.py @@ -0,0 +1,396 @@ +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", "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(1) + value = match.group(2) + masked = f"{value[:4]}***{value[-4:]}" if len(value) > 8 else "***" + return f"{key}{match.group(3)}{masked}" + + keyword_pattern = re.compile( + r"(" + "|".join(re.escape(keyword) for keyword in SECRET_KEYWORDS) + r")" + + r"\s*([:=])\s*([^\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 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 = { + key: "***MASKED***" + for key in os.environ + if any(keyword in key.lower() for keyword in SECRET_KEYWORDS) + } + 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/test/test_llama_plugin.py b/test/test_llama_plugin.py new file mode 100644 index 00000000..8d7ef3d6 --- /dev/null +++ b/test/test_llama_plugin.py @@ -0,0 +1,171 @@ +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 ABCDEFGHIJKLMNOPQRST" + masked = mask_secrets(text) + assert "sk-1234567890ABCDEFGHIJ" not in masked + assert masked.count("***") >= 1 + + +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): + 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" + 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] + stored = json.loads(report_path.read_text(encoding="utf-8")) + assert stored["network"]["127.0.0.1"] == [port] 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() From 689968a4f608c5a5b960cdcd42d51efad4176d87 Mon Sep 17 00:00:00 2001 From: Karentonoyan Date: Sat, 1 Nov 2025 01:15:24 +0100 Subject: [PATCH 3/8] Fix masking of secrets in LLaMA image chat plugin --- GramAddict/plugins/llama_image_chat.py | 36 +++++++++++++++++++++----- test/test_llama_plugin.py | 12 ++++++--- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/GramAddict/plugins/llama_image_chat.py b/GramAddict/plugins/llama_image_chat.py index d8d26b91..a4b1a88a 100644 --- a/GramAddict/plugins/llama_image_chat.py +++ b/GramAddict/plugins/llama_image_chat.py @@ -46,14 +46,36 @@ def mask_secrets(text: str) -> str: return text def _mask_keyword(match: re.Match[str]) -> str: - key = match.group(1) - value = match.group(2) - masked = f"{value[:4]}***{value[-4:]}" if len(value) > 8 else "***" - return f"{key}{match.group(3)}{masked}" + 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"\s*([:=])\s*([^\s,;]+)", + r"(?P" + "|".join(re.escape(keyword) for keyword in SECRET_KEYWORDS) + r")" + r"(?P\s*)" + r"(?P[:=])" + r"(?P\s*)" + r"(?P[^\s,;]+)", flags=re.IGNORECASE, ) @@ -61,6 +83,8 @@ def _mask_keyword(match: re.Match[str]) -> str: 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:]}" diff --git a/test/test_llama_plugin.py b/test/test_llama_plugin.py index 8d7ef3d6..648b773e 100644 --- a/test/test_llama_plugin.py +++ b/test/test_llama_plugin.py @@ -106,10 +106,16 @@ def test_wyy_creates_expected_structure(): def test_mask_secrets_masks_sensitive_values(): - text = "api_key=sk-1234567890ABCDEFGHIJ secret ABCDEFGHIJKLMNOPQRST" + text = ( + 'api_key = "sk-1234567890ABCDEFGHIJ" secret ' + "token=abcd1234 password:ZXCVBNMASDFGHJKL" + ) masked = mask_secrets(text) - assert "sk-1234567890ABCDEFGHIJ" not in masked - assert masked.count("***") >= 1 + + assert '"sk-1234567890ABCDEFGHIJ"' not in masked + assert "sk-1***GHIJ" in masked + assert "token=***" in masked + assert "password:ZXCV***HJKL" in masked def test_scan_network_detects_open_port(): From 1fdd055ca42787a73a46a9484b1ec57bf36295f9 Mon Sep 17 00:00:00 2001 From: Karentonoyan Date: Sat, 1 Nov 2025 03:27:14 +0100 Subject: [PATCH 4/8] Mask security report environment values --- GramAddict/plugins/llama_image_chat.py | 9 ++++----- test/test_llama_plugin.py | 7 ++++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/GramAddict/plugins/llama_image_chat.py b/GramAddict/plugins/llama_image_chat.py index a4b1a88a..03b9615b 100644 --- a/GramAddict/plugins/llama_image_chat.py +++ b/GramAddict/plugins/llama_image_chat.py @@ -398,11 +398,10 @@ def _security_sweep(self, args) -> Optional[dict]: ports = DEFAULT_SECURITY_PORTS open_ports = scan_network(hosts, ports) - sanitized_env = { - key: "***MASKED***" - for key in os.environ - if any(keyword in key.lower() for keyword in SECRET_KEYWORDS) - } + sanitized_env: dict[str, str] = {} + for key, value in os.environ.items(): + if any(keyword in key.lower() for keyword in SECRET_KEYWORDS): + sanitized_env[key] = mask_secrets(str(value)) report = { "network": open_ports, "environment": sanitized_env, diff --git a/test/test_llama_plugin.py b/test/test_llama_plugin.py index 648b773e..66eb0d08 100644 --- a/test/test_llama_plugin.py +++ b/test/test_llama_plugin.py @@ -143,7 +143,7 @@ def _accept_once(): assert port in results["127.0.0.1"] -def test_security_sweep_creates_report(tmp_path): +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) @@ -162,6 +162,7 @@ def _accept_once(): 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), @@ -173,5 +174,9 @@ def _accept_once(): 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") From 435efce2fc1c4d024f4d441b0232227cbdd09987 Mon Sep 17 00:00:00 2001 From: Karentonoyan Date: Sat, 1 Nov 2025 03:27:19 +0100 Subject: [PATCH 5/8] Fix secret masking boundaries --- GramAddict/plugins/llama_image_chat.py | 18 +++++++++++++-- test/test_llama_plugin.py | 32 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/GramAddict/plugins/llama_image_chat.py b/GramAddict/plugins/llama_image_chat.py index 03b9615b..61a18853 100644 --- a/GramAddict/plugins/llama_image_chat.py +++ b/GramAddict/plugins/llama_image_chat.py @@ -35,8 +35,21 @@ def build_chat_messages( return messages -SECRET_KEYWORDS = ("api_key", "token", "secret", "password", "key", "credential") +SECRET_KEYWORDS = ( + "api_key", + "api-key", + "apikey", + "token", + "secret", + "password", + "key", + "credential", +) SECRET_VALUE_PATTERN = re.compile(r"(? str: @@ -71,6 +84,7 @@ def _mask_keyword(match: re.Match[str]) -> str: 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[:=])" @@ -400,7 +414,7 @@ def _security_sweep(self, args) -> Optional[dict]: open_ports = scan_network(hosts, ports) sanitized_env: dict[str, str] = {} for key, value in os.environ.items(): - if any(keyword in key.lower() for keyword in SECRET_KEYWORDS): + if SECRET_KEYWORD_BOUNDARY.search(key) or SECRET_VALUE_PATTERN.search(str(value)): sanitized_env[key] = mask_secrets(str(value)) report = { "network": open_ports, diff --git a/test/test_llama_plugin.py b/test/test_llama_plugin.py index 66eb0d08..d68fb261 100644 --- a/test/test_llama_plugin.py +++ b/test/test_llama_plugin.py @@ -118,6 +118,15 @@ def test_mask_secrets_masks_sensitive_values(): 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) @@ -180,3 +189,26 @@ def _accept_once(): 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") From 03816bde4f3768885cd674fd176da0bf6b542198 Mon Sep 17 00:00:00 2001 From: Karentonoyan Date: Mon, 3 Nov 2025 18:12:30 +0100 Subject: [PATCH 6/8] Add intuitive routing, external AI gating, and offline storage --- extra/llama_extension/README.md | 82 ++ extra/llama_extension/__init__.py | 93 +++ extra/llama_extension/assistant.py | 1167 ++++++++++++++++++++++++++++ extra/llama_extension/config.py | 373 +++++++++ extra/llama_extension/demo.py | 839 ++++++++++++++++++++ 5 files changed, 2554 insertions(+) create mode 100644 extra/llama_extension/README.md create mode 100644 extra/llama_extension/__init__.py create mode 100644 extra/llama_extension/assistant.py create mode 100644 extra/llama_extension/config.py create mode 100644 extra/llama_extension/demo.py 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() From b3e1da13d97f7eb5052fb3cbbc8ae0cf8e817a1b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:33:56 +0000 Subject: [PATCH 7/8] Initial plan From cf34819bedb52d040168c5a1b9a073f6b920b592 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Nov 2025 22:35:03 +0000 Subject: [PATCH 8/8] Add plug-and-play setup system for GramAddict This commit transforms GramAddict into a plug-and-play bot with minimal configuration required. Users can now get started in minutes instead of spending 45-90 minutes on manual setup. Key Features Added: 1. Interactive Setup Wizard (gramaddict setup) - Automatically checks Python version - Downloads and configures ADB (Android Debug Bridge) - Detects connected Android devices - Installs UIAutomator2 on device - Generates optimized configuration files - Offers three presets: Conservative, Balanced, Aggressive 2. Health Check Command (gramaddict health-check) - Verifies all system requirements - Checks Python version and dependencies - Validates ADB installation - Confirms device connectivity - Tests UIAutomator2 functionality - Verifies Instagram app presence - Checks configuration files 3. Smart Configuration Presets - Conservative: 10-20 actions/hour, safest for new accounts - Balanced: 30-40 actions/hour, recommended default - Aggressive: 50-70 actions/hour, faster growth with higher risk - Eliminates need to understand 80+ config parameters 4. Plugin Development Template - Simple template for creating custom plugins - Clear examples and documentation - Makes extending GramAddict easier for developers 5. Updated Documentation - New QUICKSTART.md with plug-and-play instructions - Updated README.md with prominent quick start section - Reduced setup time from 45-90 minutes to 5-15 minutes Files Changed: - GramAddict/__main__.py: Added setup and health-check commands - GramAddict/core/setup_wizard.py: New interactive setup wizard - GramAddict/core/health_check.py: New system verification tool - GramAddict/plugins/plugin_template.py: Plugin development template - README.md: Added plug-and-play quick start section - QUICKSTART.md: New comprehensive quick start guide Benefits: - Automatic ADB installation eliminates most setup failures - Smart presets reduce configuration complexity - Health check helps diagnose issues quickly - Clear separation between plug-and-play and manual setup - Maintains backward compatibility with existing configs This makes GramAddict accessible to beginners while preserving advanced functionality for experienced users. --- GramAddict/__main__.py | 26 ++ GramAddict/core/health_check.py | 231 +++++++++++++ GramAddict/core/setup_wizard.py | 457 ++++++++++++++++++++++++++ GramAddict/plugins/plugin_template.py | 195 +++++++++++ QUICKSTART.md | 248 ++++++++++++++ README.md | 42 ++- 6 files changed, 1197 insertions(+), 2 deletions(-) create mode 100644 GramAddict/core/health_check.py create mode 100644 GramAddict/core/setup_wizard.py create mode 100644 GramAddict/plugins/plugin_template.py create mode 100644 QUICKSTART.md diff --git a/GramAddict/__main__.py b/GramAddict/__main__.py index 086d8348..277a9992 100644 --- a/GramAddict/__main__.py +++ b/GramAddict/__main__.py @@ -4,6 +4,8 @@ from GramAddict import __version__ from GramAddict.core.bot_flow import start_bot from GramAddict.core.download_from_github import download_from_github +from GramAddict.core.setup_wizard import run_setup_wizard +from GramAddict.core.health_check import run_health_check def cmd_init(args): @@ -78,7 +80,31 @@ def make_archive(name): print(Fore.BLUE + Style.BRIGHT + f"{os.getcwd()}\\screen_{archive_name}.zip") +def cmd_setup(args): + """Run the plug-and-play setup wizard.""" + import sys + sys.exit(run_setup_wizard()) + + +def cmd_health_check(args): + """Run system health check.""" + import sys + sys.exit(run_health_check()) + + _commands = [ + dict( + action=cmd_setup, + command="setup", + help="interactive setup wizard for plug-and-play configuration (recommended for first-time users)", + flags=[], + ), + dict( + action=cmd_health_check, + command="health-check", + help="verify system requirements and configuration", + flags=[], + ), dict( action=cmd_init, command="init", diff --git a/GramAddict/core/health_check.py b/GramAddict/core/health_check.py new file mode 100644 index 00000000..b30ea22a --- /dev/null +++ b/GramAddict/core/health_check.py @@ -0,0 +1,231 @@ +""" +GramAddict Health Check - System Verification +Checks all requirements and dependencies for GramAddict. +""" + +import os +import shutil +import subprocess +import sys +from pathlib import Path + +from colorama import Fore, Style, init + +init(autoreset=True) + + +class HealthCheck: + """System health checker for GramAddict.""" + + def __init__(self): + self.checks = [] + self.passed = 0 + self.failed = 0 + + def check(self, name: str, func): + """Run a health check.""" + try: + result, message = func() + if result: + print(f"{Fore.GREEN}✓ {name}: {message}") + self.passed += 1 + else: + print(f"{Fore.RED}✗ {name}: {message}") + self.failed += 1 + return result + except Exception as e: + print(f"{Fore.RED}✗ {name}: Error - {e}") + self.failed += 1 + return False + + def check_python_version(self): + """Check Python version.""" + version = sys.version_info + if version.major == 3 and version.minor >= 6: + return True, f"Python {version.major}.{version.minor}.{version.micro}" + else: + return ( + False, + f"Python 3.6+ required (found {version.major}.{version.minor})", + ) + + def check_dependencies(self): + """Check if required dependencies are installed.""" + required = [ + "colorama", + "configargparse", + "yaml", + "uiautomator2", + "emoji", + "requests", + ] + missing = [] + + for module in required: + try: + __import__(module) + except ImportError: + missing.append(module) + + if not missing: + return True, f"All {len(required)} required packages installed" + else: + return False, f"Missing packages: {', '.join(missing)}" + + def check_adb(self): + """Check if ADB is available.""" + adb_path = shutil.which("adb") + if adb_path: + try: + result = subprocess.run( + ["adb", "version"], capture_output=True, text=True, timeout=5 + ) + version_line = result.stdout.split("\n")[0] + return True, f"Found at {adb_path} ({version_line})" + except Exception: + return True, f"Found at {adb_path}" + else: + # Check in .gramaddict directory + gramaddict_adb = ( + Path.home() / ".gramaddict" / "platform-tools" / "platform-tools" / "adb" + ) + if gramaddict_adb.exists(): + return True, f"Found at {gramaddict_adb}" + return False, "Not found in PATH or ~/.gramaddict" + + def check_device_connection(self): + """Check if Android device is connected.""" + try: + result = subprocess.run( + ["adb", "devices"], capture_output=True, text=True, timeout=10 + ) + lines = result.stdout.strip().split("\n")[1:] + devices = [line.split()[0] for line in lines if line.strip()] + + if devices: + return True, f"{len(devices)} device(s) connected: {', '.join(devices)}" + else: + return False, "No devices connected" + except FileNotFoundError: + return False, "Cannot run 'adb devices' (ADB not found)" + except Exception as e: + return False, f"Error: {e}" + + def check_uiautomator2(self): + """Check if UIAutomator2 is installed.""" + try: + import uiautomator2 as u2 + + # Try to connect to device + try: + d = u2.connect() + info = d.info + return True, f"Installed and working (Device: {info.get('productName', 'Unknown')})" + except Exception: + return True, "Installed (device not connected or not initialized)" + except ImportError: + return False, "Package not installed" + + def check_config_exists(self): + """Check if any config files exist.""" + accounts_dir = Path("accounts") + if not accounts_dir.exists(): + return False, "No accounts directory found" + + configs = list(accounts_dir.glob("*/config.yml")) + if configs: + return True, f"Found {len(configs)} account(s) configured" + else: + return False, "No config files found in accounts/" + + def check_instagram_app(self): + """Check if Instagram app is installed on device.""" + try: + result = subprocess.run( + ["adb", "shell", "pm", "list", "packages", "instagram"], + capture_output=True, + text=True, + timeout=10, + ) + + packages = [ + line.split(":")[-1] + for line in result.stdout.split("\n") + if "instagram" in line.lower() + ] + + if "com.instagram.android" in packages: + return True, "Instagram app found on device" + elif packages: + return True, f"Instagram-like apps found: {', '.join(packages)}" + else: + return False, "Instagram app not found on device" + except FileNotFoundError: + return False, "Cannot check (ADB not available)" + except Exception as e: + return False, f"Error: {e}" + + def check_write_permissions(self): + """Check if we have write permissions.""" + test_dir = Path("accounts") + try: + test_dir.mkdir(exist_ok=True) + test_file = test_dir / ".health_check_test" + test_file.write_text("test") + test_file.unlink() + return True, "Can write to accounts directory" + except Exception as e: + return False, f"Cannot write to accounts directory: {e}" + + def run(self): + """Run all health checks.""" + print(f"\n{Fore.CYAN}{Style.BRIGHT}{'=' * 60}") + print(f"{Fore.CYAN}{Style.BRIGHT}{'GramAddict Health Check'.center(60)}") + print(f"{Fore.CYAN}{Style.BRIGHT}{'=' * 60}\n") + + print(f"{Fore.YELLOW}System Requirements:") + self.check("Python Version", self.check_python_version) + self.check("Required Packages", self.check_dependencies) + self.check("Write Permissions", self.check_write_permissions) + + print(f"\n{Fore.YELLOW}Android Setup:") + self.check("ADB Installed", self.check_adb) + self.check("Device Connected", self.check_device_connection) + self.check("UIAutomator2", self.check_uiautomator2) + self.check("Instagram App", self.check_instagram_app) + + print(f"\n{Fore.YELLOW}Configuration:") + self.check("Config Files", self.check_config_exists) + + # Summary + print(f"\n{Fore.CYAN}{Style.BRIGHT}{'=' * 60}") + total = self.passed + self.failed + if self.failed == 0: + print(f"{Fore.GREEN}{Style.BRIGHT}All {total} checks passed! ✓") + print(f"{Fore.GREEN}GramAddict is ready to use!") + print( + f"\n{Fore.WHITE}Run: {Fore.YELLOW}gramaddict run --config accounts//config.yml" + ) + else: + print( + f"{Fore.YELLOW}{Style.BRIGHT}{self.passed}/{total} checks passed, {self.failed} failed" + ) + if self.failed > 0: + print(f"\n{Fore.RED}Please fix the failed checks above.") + print( + f"{Fore.WHITE}Tip: Run {Fore.YELLOW}'gramaddict setup'{Fore.WHITE} to configure GramAddict automatically" + ) + + print(f"{Fore.CYAN}{Style.BRIGHT}{'=' * 60}\n") + + return 0 if self.failed == 0 else 1 + + +def run_health_check(): + """Entry point for health check.""" + checker = HealthCheck() + return checker.run() + + +if __name__ == "__main__": + sys.exit(run_health_check()) diff --git a/GramAddict/core/setup_wizard.py b/GramAddict/core/setup_wizard.py new file mode 100644 index 00000000..538ac858 --- /dev/null +++ b/GramAddict/core/setup_wizard.py @@ -0,0 +1,457 @@ +""" +GramAddict Setup Wizard - Plug and Play Configuration +Makes GramAddict easy to use with minimal configuration. +""" + +import os +import platform +import shutil +import subprocess +import sys +import urllib.request +import zipfile +from pathlib import Path +from typing import Optional + +import yaml +from colorama import Fore, Style, init + +init(autoreset=True) + + +class SetupWizard: + """Interactive setup wizard for GramAddict plug-and-play experience.""" + + def __init__(self): + self.system = platform.system() + self.adb_path = None + self.username = None + self.preset = "balanced" + self.accounts_dir = Path("accounts") + + def print_header(self, text: str): + """Print a formatted header.""" + print(f"\n{Fore.CYAN}{Style.BRIGHT}{'=' * 60}") + print(f"{Fore.CYAN}{Style.BRIGHT}{text.center(60)}") + print(f"{Fore.CYAN}{Style.BRIGHT}{'=' * 60}\n") + + def print_success(self, text: str): + """Print a success message.""" + print(f"{Fore.GREEN}✓ {text}") + + def print_error(self, text: str): + """Print an error message.""" + print(f"{Fore.RED}✗ {text}") + + def print_info(self, text: str): + """Print an info message.""" + print(f"{Fore.YELLOW}ℹ {text}") + + def run(self): + """Run the complete setup wizard.""" + self.print_header("GramAddict Plug & Play Setup") + print(f"{Fore.WHITE}Welcome! This wizard will configure GramAddict in minutes.") + print(f"{Fore.WHITE}Let's get started!\n") + + # Step 1: Check Python version + if not self.check_python_version(): + return False + + # Step 2: Check/Install ADB + if not self.setup_adb(): + return False + + # Step 3: Check device connection + if not self.check_device(): + return False + + # Step 4: Setup UIAutomator2 + if not self.setup_uiautomator2(): + return False + + # Step 5: Get account name + self.username = self.get_username() + + # Step 6: Choose preset + self.preset = self.choose_preset() + + # Step 7: Generate config + if not self.generate_config(): + return False + + # Step 8: Success! + self.print_success_message() + return True + + def check_python_version(self) -> bool: + """Check if Python version is compatible.""" + self.print_header("Step 1: Checking Python Version") + version = sys.version_info + if version.major == 3 and version.minor >= 6: + self.print_success( + f"Python {version.major}.{version.minor}.{version.micro} detected" + ) + return True + else: + self.print_error( + f"Python 3.6+ required, but {version.major}.{version.minor} found" + ) + return False + + def find_adb(self) -> Optional[str]: + """Try to find ADB in system PATH.""" + adb_executable = "adb.exe" if self.system == "Windows" else "adb" + adb_path = shutil.which(adb_executable) + return adb_path + + def download_adb(self) -> bool: + """Download and setup ADB platform tools.""" + self.print_info("Downloading Android Platform Tools...") + + # Determine download URL based on OS + urls = { + "Windows": "https://dl.google.com/android/repository/platform-tools-latest-windows.zip", + "Darwin": "https://dl.google.com/android/repository/platform-tools-latest-darwin.zip", + "Linux": "https://dl.google.com/android/repository/platform-tools-latest-linux.zip", + } + + url = urls.get(self.system) + if not url: + self.print_error(f"Unsupported operating system: {self.system}") + return False + + try: + # Download to temporary location + tools_dir = Path.home() / ".gramaddict" / "platform-tools" + tools_dir.mkdir(parents=True, exist_ok=True) + zip_path = tools_dir / "platform-tools.zip" + + self.print_info(f"Downloading to {tools_dir}...") + urllib.request.urlretrieve(url, zip_path) + + # Extract + self.print_info("Extracting...") + with zipfile.ZipFile(zip_path, "r") as zip_ref: + zip_ref.extractall(tools_dir) + + # Clean up zip + zip_path.unlink() + + # Set ADB path + adb_dir = tools_dir / "platform-tools" + self.adb_path = str( + adb_dir / ("adb.exe" if self.system == "Windows" else "adb") + ) + + # Make executable on Unix systems + if self.system != "Windows": + os.chmod(self.adb_path, 0o755) + + self.print_success(f"ADB installed to {self.adb_path}") + + # Add to PATH for current session + os.environ["PATH"] = f"{adb_dir}{os.pathsep}{os.environ['PATH']}" + + return True + + except Exception as e: + self.print_error(f"Failed to download ADB: {e}") + return False + + def setup_adb(self) -> bool: + """Check for ADB and install if needed.""" + self.print_header("Step 2: Setting up ADB") + + # Try to find ADB + self.adb_path = self.find_adb() + + if self.adb_path: + self.print_success(f"ADB found: {self.adb_path}") + return True + else: + self.print_info("ADB not found in system PATH") + response = ( + input( + f"{Fore.YELLOW}Would you like to download ADB automatically? (y/n): " + ) + .strip() + .lower() + ) + + if response == "y": + return self.download_adb() + else: + self.print_error( + "ADB is required. Please install it manually and try again." + ) + return False + + def check_device(self) -> bool: + """Check if Android device is connected.""" + self.print_header("Step 3: Checking Device Connection") + + try: + result = subprocess.run( + [self.adb_path or "adb", "devices"], + capture_output=True, + text=True, + timeout=10, + ) + + # Parse output + lines = result.stdout.strip().split("\n")[1:] # Skip header + devices = [line.split()[0] for line in lines if line.strip()] + + if not devices: + self.print_error("No devices connected") + self.print_info("Please connect your Android device with USB debugging enabled") + self.print_info("Then press Enter to retry...") + input() + return self.check_device() + + self.print_success(f"Device connected: {devices[0]}") + return True + + except subprocess.TimeoutExpired: + self.print_error("ADB command timed out") + return False + except FileNotFoundError: + self.print_error("ADB not found") + return False + except Exception as e: + self.print_error(f"Error checking device: {e}") + return False + + def setup_uiautomator2(self) -> bool: + """Setup UIAutomator2 on the device.""" + self.print_header("Step 4: Setting up UIAutomator2") + + try: + self.print_info("Installing UIAutomator2 on device (this may take a minute)...") + result = subprocess.run( + [sys.executable, "-m", "uiautomator2", "init"], + capture_output=True, + text=True, + timeout=120, + ) + + if result.returncode == 0: + self.print_success("UIAutomator2 installed successfully") + return True + else: + self.print_error(f"Failed to install UIAutomator2: {result.stderr}") + return False + + except subprocess.TimeoutExpired: + self.print_error("UIAutomator2 installation timed out") + return False + except Exception as e: + self.print_error(f"Error installing UIAutomator2: {e}") + return False + + def get_username(self) -> str: + """Get Instagram username from user.""" + self.print_header("Step 5: Account Configuration") + while True: + username = input(f"{Fore.YELLOW}Enter your Instagram username: ").strip() + if username: + return username + self.print_error("Username cannot be empty") + + def choose_preset(self) -> str: + """Let user choose a configuration preset.""" + self.print_header("Step 6: Choose Bot Behavior") + + print(f"{Fore.WHITE}Choose how aggressive the bot should be:\n") + print(f"{Fore.GREEN}1. Conservative (Safest, slower growth)") + print(f" • 10-20 interactions per hour") + print(f" • Long delays between actions") + print(f" • Best for new accounts or avoiding detection\n") + + print(f"{Fore.CYAN}2. Balanced (Recommended)") + print(f" • 30-40 interactions per hour") + print(f" • Natural delays") + print(f" • Good balance of growth and safety\n") + + print(f"{Fore.RED}3. Aggressive (Faster growth, higher risk)") + print(f" • 50-70 interactions per hour") + print(f" • Shorter delays") + print(f" • Not recommended for new accounts\n") + + while True: + choice = input(f"{Fore.YELLOW}Select option (1-3) [2]: ").strip() or "2" + if choice in ["1", "2", "3"]: + presets = {"1": "conservative", "2": "balanced", "3": "aggressive"} + selected = presets[choice] + self.print_success(f"Selected: {selected.capitalize()} preset") + return selected + self.print_error("Please enter 1, 2, or 3") + + def get_preset_config(self) -> dict: + """Get configuration based on preset.""" + presets = { + "conservative": { + "speed": 1, + "total_likes_limit": 50, + "total_follows_limit": 30, + "total_unfollows_limit": 30, + "total_comments_limit": 15, + "total_pm_limit": 10, + "interaction_likers_min": 1, + "interaction_likers_max": 3, + "interaction_followers_min": 1, + "interaction_followers_max": 3, + "interaction_posts_min": 1, + "interaction_posts_max": 2, + "like_percentage": 50, + "follow_percentage": 40, + "comment_percentage": 20, + }, + "balanced": { + "speed": 2, + "total_likes_limit": 150, + "total_follows_limit": 100, + "total_unfollows_limit": 100, + "total_comments_limit": 30, + "total_pm_limit": 20, + "interaction_likers_min": 2, + "interaction_likers_max": 4, + "interaction_followers_min": 2, + "interaction_followers_max": 4, + "interaction_posts_min": 2, + "interaction_posts_max": 4, + "like_percentage": 70, + "follow_percentage": 50, + "comment_percentage": 30, + }, + "aggressive": { + "speed": 3, + "total_likes_limit": 300, + "total_follows_limit": 200, + "total_unfollows_limit": 200, + "total_comments_limit": 50, + "total_pm_limit": 30, + "interaction_likers_min": 3, + "interaction_likers_max": 6, + "interaction_followers_min": 3, + "interaction_followers_max": 6, + "interaction_posts_min": 3, + "interaction_posts_max": 5, + "like_percentage": 80, + "follow_percentage": 60, + "comment_percentage": 40, + }, + } + return presets.get(self.preset, presets["balanced"]) + + def generate_config(self) -> bool: + """Generate configuration files.""" + self.print_header("Step 7: Generating Configuration") + + try: + # Create accounts directory + account_dir = self.accounts_dir / self.username + account_dir.mkdir(parents=True, exist_ok=True) + + # Get preset config + preset_cfg = self.get_preset_config() + + # Generate minimal config.yml + config = { + "username": self.username, + "app_id": "com.instagram.android", + "speed": preset_cfg["speed"], + "debug": False, + "close_apps": True, + "screen_sleep": False, + "disable_block_detection": False, + # Session limits + "total_likes_limit": preset_cfg["total_likes_limit"], + "total_follows_limit": preset_cfg["total_follows_limit"], + "total_unfollows_limit": preset_cfg["total_unfollows_limit"], + "total_comments_limit": preset_cfg["total_comments_limit"], + "total_pm_limit": preset_cfg["total_pm_limit"], + # Interaction settings + "like_percentage": preset_cfg["like_percentage"], + "follow_percentage": preset_cfg["follow_percentage"], + "comment_percentage": preset_cfg["comment_percentage"], + # Default interaction + "interact_from_profile_percentage": 100, + } + + config_path = account_dir / "config.yml" + with open(config_path, "w", encoding="utf-8") as f: + f.write(f"# GramAddict Config - {self.preset.capitalize()} Preset\n") + f.write(f"# Auto-generated by Setup Wizard\n\n") + yaml.dump(config, f, default_flow_style=False, sort_keys=False) + + self.print_success(f"Config saved: {config_path}") + + # Generate minimal filters.yml + filters = { + "skip_business": False, + "skip_non_business": False, + "skip_following": True, + "skip_follower": False, + "min_followers": 50, + "max_followers": 10000, + "min_followings": 50, + "max_followings": 5000, + "min_potency_ratio": 0.5, + "follow_private_or_empty": False, + } + + filters_path = account_dir / "filters.yml" + with open(filters_path, "w", encoding="utf-8") as f: + f.write("# GramAddict Filters\n") + f.write("# Auto-generated by Setup Wizard\n\n") + yaml.dump(filters, f, default_flow_style=False, sort_keys=False) + + self.print_success(f"Filters saved: {filters_path}") + + # Create empty list files + for filename in ["whitelist.txt", "blacklist.txt", "comments_list.txt"]: + filepath = account_dir / filename + filepath.touch() + + self.print_success("Created list files (whitelist, blacklist, comments)") + + return True + + except Exception as e: + self.print_error(f"Failed to generate config: {e}") + return False + + def print_success_message(self): + """Print final success message with next steps.""" + self.print_header("Setup Complete!") + + print(f"{Fore.GREEN}{Style.BRIGHT}✓ GramAddict is ready to use!\n") + + print(f"{Fore.CYAN}Next Steps:") + print(f"{Fore.WHITE}1. Start the bot:") + print( + f" {Fore.YELLOW}gramaddict run --config accounts/{self.username}/config.yml\n" + ) + + print(f"{Fore.WHITE}2. Optional: Customize your configuration:") + print(f" {Fore.YELLOW}accounts/{self.username}/config.yml") + print(f" {Fore.YELLOW}accounts/{self.username}/filters.yml\n") + + print(f"{Fore.WHITE}3. Add interactions (example):") + print( + f" {Fore.YELLOW}gramaddict run --config accounts/{self.username}/config.yml --interact @instagram\n" + ) + + print(f"{Fore.CYAN}Tip: Run 'gramaddict health-check' to verify everything is working!\n") + + +def run_setup_wizard(): + """Entry point for setup wizard.""" + wizard = SetupWizard() + success = wizard.run() + return 0 if success else 1 + + +if __name__ == "__main__": + sys.exit(run_setup_wizard()) diff --git a/GramAddict/plugins/plugin_template.py b/GramAddict/plugins/plugin_template.py new file mode 100644 index 00000000..ccb24c8b --- /dev/null +++ b/GramAddict/plugins/plugin_template.py @@ -0,0 +1,195 @@ +""" +GramAddict Plugin Template + +This template shows how to create a new plugin for GramAddict. +Copy this file and modify it to add new bot functionality. + +Plugin Basics: +1. All plugins must inherit from the Plugin base class +2. Define arguments in the arguments list +3. Implement the run() method with your bot logic +4. Save the file as interact_.py or action_.py +""" + +from GramAddict.core.plugin_loader import Plugin + + +class MyCustomPlugin(Plugin): + """ + Example plugin that demonstrates the basic structure. + + Replace this with your plugin description. + """ + + def __init__(self): + super().__init__() + # Define plugin name + self.description = "Example plugin template" + # Define CLI arguments for this plugin + self.arguments = [ + { + # Argument name (will be accessible as --my-action) + "arg": "--my-action", + # Number of arguments: "?" for optional, "+" for one or more, "*" for zero or more + "nargs": None, + # Help text shown in CLI + "help": "example action that does something cool", + # Metadata key (optional) + "metavar": "value", + # Default value + "default": None, + # Mark as an operation (makes it show up as a main bot action) + "operation": True, + } + ] + + def run(self, config, plugin, followers_now, following_now, time_left): + """ + Main plugin execution method. + + This method is called when the plugin is activated. + + Args: + config: Configuration object with all settings + plugin: The plugin instance (self) + followers_now: Current follower count + following_now: Current following count + time_left: Time remaining in session + + Returns: + None + """ + # Get the argument value from config + action_value = config.args.my_action # Note: dashes become underscores + + if action_value is None: + # Plugin not activated + return + + # Your plugin logic goes here + print(f"Running MyCustomPlugin with value: {action_value}") + + # Example: Interact with device + # device = config.device + # device.click(x=500, y=500) + + # Example: Access storage + # from GramAddict.core.storage import Storage + # storage = Storage(config.username) + # storage.add_interacted_user("username123") + + # Example: Log information + # logger.info("Doing something cool...") + + pass + + +# EXAMPLES OF COMMON PLUGIN PATTERNS +# ----------------------------------- + +# 1. INTERACTION PLUGIN +# Interacts with users, posts, hashtags, etc. +class InteractExamplePlugin(Plugin): + """Example interaction plugin.""" + + def __init__(self): + super().__init__() + self.description = "Interact with example target" + self.arguments = [ + { + "arg": "--interact-example", + "nargs": "+", + "help": "list of targets to interact with", + "metavar": ("target1", "target2"), + "default": None, + "operation": True, + } + ] + + def run(self, config, plugin, followers_now, following_now, time_left): + targets = config.args.interact_example + if targets is None: + return + + for target in targets: + print(f"Interacting with {target}...") + # Your interaction logic here + pass + + +# 2. ACTION PLUGIN +# Performs actions like unfollowing, removing followers, etc. +class ActionExamplePlugin(Plugin): + """Example action plugin.""" + + def __init__(self): + super().__init__() + self.description = "Perform example action" + self.arguments = [ + { + "arg": "--action-example", + "nargs": None, + "help": "perform example action", + "metavar": None, + "default": None, + "operation": True, + } + ] + + def run(self, config, plugin, followers_now, following_now, time_left): + if config.args.action_example is None: + return + + print("Performing action...") + # Your action logic here + pass + + +# 3. FILTER PLUGIN +# Adds filtering capabilities +class FilterExamplePlugin(Plugin): + """Example filter plugin.""" + + def __init__(self): + super().__init__() + self.description = "Example filter" + self.arguments = [ + { + "arg": "--filter-example", + "nargs": None, + "help": "enable example filter", + "metavar": "value", + "default": None, + "operation": False, # Not a main operation + } + ] + + def run(self, config, plugin, followers_now, following_now, time_left): + # Filters typically don't have run() logic + # They're checked during interactions + pass + + +# PLUGIN NAMING CONVENTIONS +# ------------------------- +# interact_*.py - Interaction plugins (follow, like, comment) +# action_*.py - Action plugins (unfollow, remove, scrape) +# core_*.py - Core functionality (filters, arguments) +# *_analytics.py - Analytics and reporting +# *.py - Utility plugins + +# HOW TO USE YOUR PLUGIN +# ----------------------- +# 1. Save this file in GramAddict/plugins/ +# 2. Name it according to convention (e.g., interact_my_feature.py) +# 3. Run: gramaddict run --config config.yml --my-action value +# 4. The plugin will be automatically discovered and loaded! + +# TIPS +# ---- +# - Look at existing plugins in GramAddict/plugins/ for examples +# - Use logger for output instead of print() +# - Access device via config.device +# - Access storage via Storage(config.username) +# - Test with --debug flag for verbose logging +# - Use filters from config.args to respect user preferences diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 00000000..aad2fe1f --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,248 @@ +# GramAddict Plug & Play Quick Start 🚀 + +GramAddict is now **plug and play**! Get started in minutes with our interactive setup wizard. + +## Prerequisites + +- **Python 3.6+** installed (Python 3.10 currently not supported) +- **Android device or emulator** with Instagram app installed +- **USB cable** (for physical devices) or emulator running + +## Installation + +### Step 1: Install GramAddict + +```bash +pip3 install GramAddict +``` + +### Step 2: Run the Setup Wizard + +```bash +gramaddict setup +``` + +The wizard will guide you through: + +1. **Python Version Check** - Verifies you have Python 3.6+ +2. **ADB Setup** - Automatically downloads and configures Android Debug Bridge +3. **Device Detection** - Finds your connected Android device +4. **UIAutomator2 Setup** - Installs automation framework on your device +5. **Account Configuration** - Gets your Instagram username +6. **Bot Behavior Preset** - Choose your preferred automation level: + - **Conservative** (Safest): 10-20 actions/hour, longer delays + - **Balanced** (Recommended): 30-40 actions/hour, natural delays + - **Aggressive** (Risky): 50-70 actions/hour, shorter delays +7. **Config Generation** - Creates optimized configuration files + +### Step 3: Start the Bot + +After setup completes, run: + +```bash +gramaddict run --config accounts/yourusername/config.yml --interact @instagram +``` + +Replace `yourusername` with your actual Instagram username. + +## Verify Your Setup + +At any time, check if everything is working: + +```bash +gramaddict health-check +``` + +This will verify: +- ✓ Python version +- ✓ Required packages installed +- ✓ ADB installed and accessible +- ✓ Device connected +- ✓ UIAutomator2 working +- ✓ Instagram app on device +- ✓ Config files present + +## Configuration Presets + +The setup wizard creates optimized configurations based on your chosen preset: + +### Conservative (Safest) +```yaml +speed: 1 +total_likes_limit: 50 +total_follows_limit: 30 +total_comments_limit: 15 +``` +Best for new accounts or maximum safety. + +### Balanced (Recommended) +```yaml +speed: 2 +total_likes_limit: 150 +total_follows_limit: 100 +total_comments_limit: 30 +``` +Good balance between growth and safety. + +### Aggressive (Higher Risk) +```yaml +speed: 3 +total_likes_limit: 300 +total_follows_limit: 200 +total_comments_limit: 50 +``` +Faster growth but higher detection risk. Not recommended for new accounts. + +## Next Steps + +### 1. Customize Your Bot (Optional) + +Edit configuration files in `accounts/yourusername/`: +- `config.yml` - Main bot settings +- `filters.yml` - Profile filtering rules +- `comments_list.txt` - Comments to post +- `whitelist.txt` - Accounts to keep when unfollowing +- `blacklist.txt` - Accounts to skip + +### 2. Add Interactions + +The bot can interact with: + +```bash +# Interact with a user's followers +gramaddict run --config accounts/yourusername/config.yml --interact @username + +# Interact with hashtag likers +gramaddict run --config accounts/yourusername/config.yml --interact #hashtag + +# Interact with place likers +gramaddict run --config accounts/yourusername/config.yml --interact place123456 + +# Multiple interactions +gramaddict run --config accounts/yourusername/config.yml \ + --interact @user1 @user2 #hashtag1 #hashtag2 +``` + +### 3. Unfollow Non-Followers + +```bash +# Unfollow people who don't follow back +gramaddict run --config accounts/yourusername/config.yml \ + --unfollow non-followers + +# Unfollow all +gramaddict run --config accounts/yourusername/config.yml \ + --unfollow any +``` + +### 4. Enable Telegram Reports (Optional) + +Get bot activity reports via Telegram: + +1. Edit `accounts/yourusername/telegram.yml` +2. Add your bot token and chat ID +3. Follow [this guide](https://docs.gramaddict.org/#/configuration?id=telegram-reports) + +## Troubleshooting + +### Device Not Detected + +1. Enable **USB Debugging** on your device: + - Settings → About Phone → Tap "Build Number" 7 times + - Settings → Developer Options → Enable USB Debugging + +2. Accept the "Allow USB debugging" prompt on your device + +3. Run `adb devices` to verify connection + +### ADB Not Found + +If automatic download fails: +1. Manually download [platform-tools](https://developer.android.com/studio/releases/platform-tools) +2. Extract to a permanent location +3. Add to PATH environment variable +4. Run `gramaddict setup` again + +### UIAutomator2 Fails + +```bash +# Manually initialize UIAutomator2 +python3 -m uiautomator2 init +``` + +### Instagram Not in English + +The bot requires Instagram to be in English: +1. Open Instagram app +2. Profile → Settings → Language +3. Select English + +### Still Having Issues? + +Run the health check to diagnose: +```bash +gramaddict health-check +``` + +Or join our [Discord community](https://discord.gg/NK8PNEFGFF) for help! + +## Advanced Usage + +### Multiple Accounts + +Run setup for each account: +```bash +gramaddict setup # Follow prompts for account1 +gramaddict setup # Follow prompts for account2 +``` + +### Custom Config + +For advanced users, skip the wizard: +```bash +gramaddict init yourusername +``` +Then manually edit the config files. + +### Schedule Bot Sessions + +Use cron (Linux/Mac) or Task Scheduler (Windows) to automate: +```bash +# Example cron job (runs every 2 hours) +0 */2 * * * cd /path/to/gramaddict && gramaddict run --config accounts/user/config.yml +``` + +## Safety Tips + +- ⚠️ Start with **Conservative** preset for new accounts +- ⚠️ Don't run 24/7 - act like a human! +- ⚠️ Gradually increase activity over time +- ⚠️ Use filters to target relevant accounts +- ⚠️ Monitor for action blocks or bans +- ⚠️ Never share your Instagram password + +## What's Different from Manual Setup? + +The plug-and-play wizard automates: +- ✓ ADB download and configuration +- ✓ PATH environment variable setup +- ✓ UIAutomator2 installation +- ✓ Config file generation with smart defaults +- ✓ Preset-based optimization + +Old way: **7+ manual steps, 45-90 minutes** +New way: **2 commands, 5-15 minutes** + +## Documentation + +Full documentation: [docs.gramaddict.org](https://docs.gramaddict.org) + +## Support + +- [Discord Community](https://discord.gg/NK8PNEFGFF) +- [GitHub Issues](https://github.com/GramAddict/bot/issues) +- [Documentation](https://docs.gramaddict.org) + +--- + +**Disclaimer**: This project comes with no guarantee or warranty. You are responsible for whatever happens from using this project. It is possible to get soft or hard banned by using this project if you are not careful. diff --git a/README.md b/README.md index 9662b8fe..7142c16a 100644 --- a/README.md +++ b/README.md @@ -144,8 +144,46 @@ You can get __reports__ through telegram from your bot activities! Cool, isn't it? 💦 # Quick start -Now that you're there you still don't know how to install that whole thing. -I'll try to help you accomplish that with a short tutorial. The full is available [here](https://docs.gramaddict.org/#/quickstart). + +## 🚀 NEW: Plug & Play Setup (Easiest Way!) + +We now have an **interactive setup wizard** that does everything automatically! Perfect for beginners. + +### Just 3 steps: +1. **Install GramAddict:** + ```bash + pip3 install GramAddict + ``` + +2. **Run the setup wizard:** + ```bash + gramaddict setup + ``` + The wizard will: + - ✓ Check your Python version + - ✓ Download and configure ADB automatically + - ✓ Detect your Android device + - ✓ Setup UIAutomator2 + - ✓ Generate optimal configuration for you + - ✓ Get you ready in minutes! + +3. **Start the bot:** + ```bash + gramaddict run --config accounts/yourusername/config.yml + ``` + +### Verify everything works: +```bash +gramaddict health-check +``` + +That's it! 🎉 No need to manually configure ADB or edit YAML files. + +--- + +## 📖 Manual Setup (Traditional Way) + +If you prefer to set things up manually, follow this tutorial. The full is available [here](https://docs.gramaddict.org/#/quickstart). ## What do you need: - a computer (with Windows, macOS or Linux)