Add global research verification policy
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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.
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user