From b2ea53c383d0f049a10a1ef55e9040a967286498 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Mon, 31 Aug 2026 22:35:09 +0200 Subject: [PATCH] Add global research verification policy --- ATHENA.md | 10 ++++++ compose.yaml | 2 ++ config/global-system-policy.txt | 29 +++++++++++++++ dev/test_router_support.py | 48 +++++++++++++++++++++++++ router/ai_profile_router.py | 62 +++++++++++++++++++++++++++++++++ 5 files changed, 151 insertions(+) create mode 100644 config/global-system-policy.txt diff --git a/ATHENA.md b/ATHENA.md index 682a42d..56eff27 100644 --- a/ATHENA.md +++ b/ATHENA.md @@ -62,6 +62,16 @@ Qwen-Profil wird vom Profile Controller verwaltet. Die verbindlichen Werte stehen in `config/profile-matrix.json` und `docs/STANDARD_PROFILE_MATRIX.md`. +## Globale Modellrichtlinie + +`config/global-system-policy.txt` wird vom Profile Router allen Textanfragen +über `/v1/chat/completions` und `/v1/responses` vorangestellt. Sie gilt damit +für alle Hermes-Profile und andere Clients, die den Athena-Router verwenden. +Der Router liest die Datei bei jeder Anfrage neu; spätere Textänderungen +benötigen nach der erstmaligen Bereitstellung keinen Container-Neustart. +Clients außerhalb des Routers und Werkzeuge, die ein Frontend nicht anbietet, +werden dadurch nicht erfasst. + ## Werkzeuge Portable Werkzeuge gehören auf Unraid in eigene, per DockerMan verwaltete diff --git a/compose.yaml b/compose.yaml index 09ea0d5..5233077 100644 --- a/compose.yaml +++ b/compose.yaml @@ -622,6 +622,7 @@ services: tmpfs: ["/tmp:size=256m"] volumes: - ./router/router_profiles.json:/etc/mike-ai/router-profiles.json:ro + - ./config/global-system-policy.txt:/etc/mike-ai/global-system-policy.txt:ro - router-state:/var/lib/mike-ai-profile-router - router-images:/data/images environment: @@ -645,6 +646,7 @@ services: # consume the complete context before yielding visible output. MAX_GENERATION_TOKENS: "8192" DEFAULT_REASONING_EFFORT: "${DEFAULT_REASONING_EFFORT:-off}" + GLOBAL_SYSTEM_POLICY_FILE: /etc/mike-ai/global-system-policy.txt IMAGE_DIR: /data/images IMAGE_WORKER_URL: http://image-worker:8086 IMAGE_WORKER_TOKEN: "${CONTROLLER_TOKEN:?CONTROLLER_TOKEN is required}" diff --git a/config/global-system-policy.txt b/config/global-system-policy.txt new file mode 100644 index 0000000..cc7923a --- /dev/null +++ b/config/global-system-policy.txt @@ -0,0 +1,29 @@ +## Mandatory Research and Verification Policy + +When an answer, decision, or planned action depends on external facts and uncertainty could materially affect the result, verify the relevant information before proceeding. + +Never infer the purpose or capabilities of an unfamiliar product, project, repository, application, container, image, service, package, or proper name from its name alone. + +For unfamiliar software or services: + +1. Inspect available local metadata such as the image name, labels, project URL, Compose file, package metadata, or README. +2. If its identity or capabilities remain unclear, use an available web, documentation, source-code, or research tool. +3. Base the answer on verified information and clearly distinguish facts from inference. + +Research is required when: + +- the information may have changed recently; +- you are unfamiliar with an error, parameter, API, feature, path, product, or technical procedure; +- compatibility, security, migration, or configuration details are unclear; +- your first or second reasonable attempt has failed; +- an incorrect assumption could cause damage, data loss, downtime, or significant wasted effort. + +Prefer authoritative primary sources such as official documentation, upstream source code, release notes, specifications, and vendor documentation. Do not invent commands, parameters, endpoints, file paths, capabilities, or configuration options. + +If no suitable research tool is available, clearly state what is uncertain. Ask the user before performing an action that could be harmful or difficult to reverse. + +Do not perform unnecessary research when the answer can be derived reliably from information supplied by the user, local documentation, direct observation, or straightforward reasoning. + +Never include passwords, API keys, tokens, private messages, confidential file contents, or other sensitive information in web searches or requests to external services. + +Research is a verification mechanism, not a substitute for reasoning. Review the sources, reconcile conflicting information, and explain any remaining uncertainty. diff --git a/dev/test_router_support.py b/dev/test_router_support.py index 64f1c3a..2896cf7 100644 --- a/dev/test_router_support.py +++ b/dev/test_router_support.py @@ -26,6 +26,7 @@ from router_support import ( # noqa: E402 from ai_profile_router import ( # noqa: E402 _cap_chat_generation, _context_matches, + _inject_global_system_policy, _normalize_chat_image, _normalize_chat_images, _normalize_llamacpp_reasoning, @@ -201,6 +202,53 @@ class ChatGenerationLimitTests(unittest.TestCase): ) +class GlobalSystemPolicyTests(unittest.TestCase): + def _inject(self, request: dict, path: str, + policy: str = "Verify facts.") -> dict: + with patch("ai_profile_router._load_global_system_policy", + return_value=policy): + return _inject_global_system_policy(request, path) + + def test_chat_policy_precedes_existing_system_prompt(self) -> None: + request = {"messages": [ + {"role": "system", "content": "Client policy."}, + {"role": "user", "content": "Hello"}, + ]} + normalized = self._inject(request, "/v1/chat/completions") + self.assertEqual( + normalized["messages"][0]["content"], + "Verify facts.\n\nClient policy.", + ) + + def test_chat_policy_is_inserted_without_system_prompt(self) -> None: + request = {"messages": [{"role": "user", "content": "Hello"}]} + normalized = self._inject(request, "/v1/chat/completions") + self.assertEqual(normalized["messages"][0], { + "role": "system", "content": "Verify facts.", + }) + + def test_policy_is_not_duplicated(self) -> None: + request = {"messages": [{ + "role": "system", "content": "Verify facts.\n\nClient policy.", + }]} + normalized = self._inject(request, "/v1/chat/completions") + self.assertEqual( + normalized["messages"][0]["content"].count("Verify facts."), 1) + + def test_responses_policy_precedes_instructions(self) -> None: + request = {"instructions": "Client policy.", "input": "Hello"} + normalized = self._inject(request, "/v1/responses") + self.assertEqual( + normalized["instructions"], + "Verify facts.\n\nClient policy.", + ) + + def test_unrelated_endpoint_is_unchanged(self) -> None: + request = {"prompt": "Draw a cat"} + self.assertEqual( + self._inject(request, "/v1/images/generations"), request) + + class RetentionTests(unittest.TestCase): def test_oldest_pairs_are_removed(self) -> None: with tempfile.TemporaryDirectory() as temp: diff --git a/router/ai_profile_router.py b/router/ai_profile_router.py index 6121d2b..f247bbb 100755 --- a/router/ai_profile_router.py +++ b/router/ai_profile_router.py @@ -121,6 +121,8 @@ POLL_INTERVAL = float(os.environ.get("POLL_INTERVAL", "2")) # s, Polling MAX_GENERATION_TOKENS = int(os.environ.get("MAX_GENERATION_TOKENS", "8192")) DEFAULT_REASONING_EFFORT = os.environ.get( "DEFAULT_REASONING_EFFORT", "off").strip().lower() +GLOBAL_SYSTEM_POLICY_FILE = os.environ.get( + "GLOBAL_SYSTEM_POLICY_FILE", "").strip() # --- Bildgenerierung und Referenzbild-Bearbeitung (FLUX.2 Klein 4B) --- LLAMA_SERVICE = os.environ.get("LLAMA_SERVICE", "mike-ai-llama-ui.service") @@ -1270,6 +1272,57 @@ _REASONING_EFFORT_MAP = { _REASONING_OFF = {"", "none", "off", "disabled", "false"} +def _load_global_system_policy() -> str: + """Load the static cross-client platform policy. + + The file is read for every request so operators can revise the policy + without rebuilding or restarting the router. Its contents remain stable + between edits and therefore remain friendly to upstream prompt caches. + """ + if not GLOBAL_SYSTEM_POLICY_FILE: + return "" + try: + with open(GLOBAL_SYSTEM_POLICY_FILE, encoding="utf-8") as handle: + return handle.read().strip() + except OSError as exc: + raise ValueError( + f"globale Systemrichtlinie nicht lesbar: {exc}") from exc + + +def _inject_global_system_policy(data: dict, path: str) -> dict: + """Prepend the shared policy to OpenAI chat and Responses requests.""" + policy = _load_global_system_policy() + if not policy: + return data + + if path == "/v1/chat/completions": + messages = data.get("messages") + if not isinstance(messages, list): + return data + if messages and isinstance(messages[0], dict) and ( + messages[0].get("role") == "system" + and isinstance(messages[0].get("content"), str)): + existing = messages[0]["content"] + if policy not in existing: + messages[0]["content"] = f"{policy}\n\n{existing}" + elif not any( + isinstance(message, dict) + and message.get("role") == "system" + and message.get("content") == policy + for message in messages): + messages.insert(0, {"role": "system", "content": policy}) + return data + + if path == "/v1/responses": + instructions = data.get("instructions") + if isinstance(instructions, str) and instructions: + if policy not in instructions: + data["instructions"] = f"{policy}\n\n{instructions}" + elif instructions is None or instructions == "": + data["instructions"] = policy + return data + + def _normalize_llamacpp_reasoning(data: dict) -> dict: """Mappt OpenAI/Hermes-Reasoning auf llama.cpp-Template-Parameter. @@ -2203,6 +2256,15 @@ class Handler(BaseHTTPRequestHandler): "invalid_request_error", "unknown_model") return + if path in {"/v1/chat/completions", "/v1/responses"}: + try: + data = _inject_global_system_policy(data, path) + except ValueError as exc: + self._send_error(500, str(exc), "server_error", + "system_policy_unavailable") + return + body = json.dumps(data).encode() + # Alle modellbezogenen Requests erhalten eine atomare Lease. Damit # kann kein zweiter Client zwischen Profilwahl und Upstream-Request das # Modell austauschen. Vision-Vorbereitung gehört zur selben Transaktion.