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
|
Die verbindlichen Werte stehen in `config/profile-matrix.json` und
|
||||||
`docs/STANDARD_PROFILE_MATRIX.md`.
|
`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
|
## Werkzeuge
|
||||||
|
|
||||||
Portable Werkzeuge gehören auf Unraid in eigene, per DockerMan verwaltete
|
Portable Werkzeuge gehören auf Unraid in eigene, per DockerMan verwaltete
|
||||||
|
|||||||
@@ -622,6 +622,7 @@ services:
|
|||||||
tmpfs: ["/tmp:size=256m"]
|
tmpfs: ["/tmp:size=256m"]
|
||||||
volumes:
|
volumes:
|
||||||
- ./router/router_profiles.json:/etc/mike-ai/router-profiles.json:ro
|
- ./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-state:/var/lib/mike-ai-profile-router
|
||||||
- router-images:/data/images
|
- router-images:/data/images
|
||||||
environment:
|
environment:
|
||||||
@@ -645,6 +646,7 @@ services:
|
|||||||
# consume the complete context before yielding visible output.
|
# consume the complete context before yielding visible output.
|
||||||
MAX_GENERATION_TOKENS: "8192"
|
MAX_GENERATION_TOKENS: "8192"
|
||||||
DEFAULT_REASONING_EFFORT: "${DEFAULT_REASONING_EFFORT:-off}"
|
DEFAULT_REASONING_EFFORT: "${DEFAULT_REASONING_EFFORT:-off}"
|
||||||
|
GLOBAL_SYSTEM_POLICY_FILE: /etc/mike-ai/global-system-policy.txt
|
||||||
IMAGE_DIR: /data/images
|
IMAGE_DIR: /data/images
|
||||||
IMAGE_WORKER_URL: http://image-worker:8086
|
IMAGE_WORKER_URL: http://image-worker:8086
|
||||||
IMAGE_WORKER_TOKEN: "${CONTROLLER_TOKEN:?CONTROLLER_TOKEN is required}"
|
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
|
from ai_profile_router import ( # noqa: E402
|
||||||
_cap_chat_generation,
|
_cap_chat_generation,
|
||||||
_context_matches,
|
_context_matches,
|
||||||
|
_inject_global_system_policy,
|
||||||
_normalize_chat_image,
|
_normalize_chat_image,
|
||||||
_normalize_chat_images,
|
_normalize_chat_images,
|
||||||
_normalize_llamacpp_reasoning,
|
_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):
|
class RetentionTests(unittest.TestCase):
|
||||||
def test_oldest_pairs_are_removed(self) -> None:
|
def test_oldest_pairs_are_removed(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
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"))
|
MAX_GENERATION_TOKENS = int(os.environ.get("MAX_GENERATION_TOKENS", "8192"))
|
||||||
DEFAULT_REASONING_EFFORT = os.environ.get(
|
DEFAULT_REASONING_EFFORT = os.environ.get(
|
||||||
"DEFAULT_REASONING_EFFORT", "off").strip().lower()
|
"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) ---
|
# --- Bildgenerierung und Referenzbild-Bearbeitung (FLUX.2 Klein 4B) ---
|
||||||
LLAMA_SERVICE = os.environ.get("LLAMA_SERVICE", "mike-ai-llama-ui.service")
|
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"}
|
_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:
|
def _normalize_llamacpp_reasoning(data: dict) -> dict:
|
||||||
"""Mappt OpenAI/Hermes-Reasoning auf llama.cpp-Template-Parameter.
|
"""Mappt OpenAI/Hermes-Reasoning auf llama.cpp-Template-Parameter.
|
||||||
|
|
||||||
@@ -2203,6 +2256,15 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
"invalid_request_error", "unknown_model")
|
"invalid_request_error", "unknown_model")
|
||||||
return
|
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
|
# Alle modellbezogenen Requests erhalten eine atomare Lease. Damit
|
||||||
# kann kein zweiter Client zwischen Profilwahl und Upstream-Request das
|
# kann kein zweiter Client zwischen Profilwahl und Upstream-Request das
|
||||||
# Modell austauschen. Vision-Vorbereitung gehört zur selben Transaktion.
|
# Modell austauschen. Vision-Vorbereitung gehört zur selben Transaktion.
|
||||||
|
|||||||
Reference in New Issue
Block a user