diff --git a/CHANGES.md b/CHANGES.md index f2d9869..d433e59 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,45 @@ # Changelog +## [0.1.9] - 2026-06-24 + +### Summary + +Enterprise-level answer governance was added directly to `PromptConfig`. + +### Changes + +1. **Response language control** (`prompt_orchestrator/config/prompt_config.py`) +- Added `response_language: ru | en | auto` +- Default is `ru` for Russian-first enterprise flows + +2. **Strict output contract** (`prompt_orchestrator/config/prompt_config.py`) +- Added `OutputContractConfig` +- Added `output_contract` field in `PromptConfig` +- Default mode is `json_markdown` +- Default strictness is enabled (`strict=True`) + +3. **Tool-calling policy** (`prompt_orchestrator/config/prompt_config.py`) +- Added `ToolCallingPolicyConfig` +- Added `tool_calling_policy` field in `PromptConfig` +- Default policy allows tool usage (`mode="allow"`) +- Added per-policy limits and guard flags (`max_calls`, JSON args/result acknowledgement) + +4. **Prompt rendering updates** +- `render_static_header()` now includes: + - Response Language + - Output Contract + - Tool Calling Policy + +5. **Public API exports** +- Added exports in: + - `prompt_orchestrator/config/__init__.py` + - `prompt_orchestrator/__init__.py` + for `OutputContractConfig` and `ToolCallingPolicyConfig` + +6. **Docs and tests** +- README updated with enterprise configuration examples +- Core tests extended to validate defaults and rendering + ## [0.1.0] - 2026-05-29 ### Summary diff --git a/README.md b/README.md index 53fe2d6..f1742bd 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,46 @@ Use it as a panel/query blueprint in SigNoz to create a dashboard for prompt bui - `ModuleConfig`: full module config in one object - `ConfigStore`: mutable config holder (`get`, `set_config`, `as_dict`) +### Enterprise Prompt Controls + +`PromptConfig` now includes enterprise-level controls for answer governance: + +- `response_language`: `ru | en | auto` (default: `ru`) +- `output_contract`: strict output contract configuration + - default `mode="json_markdown"` + - default `strict=True` + - default schema hint for enterprise review payload +- `tool_calling_policy`: tool usage policy for downstream tool-aware agents + - `mode`: `allow | deny | allowlist` (default: `allow`) + - `max_calls` (default: `8`) + - `allowed_tools` for allowlist mode + - JSON args/result acknowledgement guard flags + +Example: + +```python +from prompt_orchestrator import PromptConfig, OutputContractConfig, ToolCallingPolicyConfig + +cfg = PromptConfig( + system_prompt="Вы корпоративный ассистент.", + role="Ревьюер документации", + task="Сформируйте проверяемый ответ на русском.", + constraints=["Не придумывайте факты", "Всегда давайте ссылки/цитаты"], + output_format="Markdown", + examples=[], + response_language="ru", + output_contract=OutputContractConfig( + mode="json_markdown", + strict=True, + schema_hint='{"summary":"str","findings":["str"],"risks":["str"],"actions":["str"],"citations":["str"]}', + ), + tool_calling_policy=ToolCallingPolicyConfig( + mode="allow", + max_calls=8, + ), +) +``` + ## Safety Engine The safety layer is configured from [prompt_orchestrator/safety/threats.json](prompt_orchestrator/safety/threats.json). The catalog is grouped by threat family, and each family has its own weight so the final severity is still computed by the maximum matched threat score. diff --git a/prompt_orchestrator/__init__.py b/prompt_orchestrator/__init__.py index 67f245b..68c4d0b 100644 --- a/prompt_orchestrator/__init__.py +++ b/prompt_orchestrator/__init__.py @@ -6,7 +6,7 @@ from .cache.local_ttl import LocalTTLCacheBackend from .config.config_store import ConfigStore from .config.module_config import ModuleConfig -from .config.prompt_config import PromptConfig +from .config.prompt_config import OutputContractConfig, PromptConfig, ToolCallingPolicyConfig from .config.settings import OrchestratorSettings from .context.manager import PromptContextManager from .context.state import DocChunk, Message, PromptContextState @@ -35,6 +35,7 @@ "OllamaSummaryClient", "OpenAIConfig", "OpenAISummaryClient", + "OutputContractConfig", "OrchestratedPrompt", "OrchestratorSettings", "PromptAnalyzer", @@ -50,6 +51,7 @@ "SummaryLLM", "SummaryLLMConfig", "TokenCounter", + "ToolCallingPolicyConfig", "init_telemetry", "shutdown_telemetry", ] diff --git a/prompt_orchestrator/config/__init__.py b/prompt_orchestrator/config/__init__.py index 4fe9166..121099d 100644 --- a/prompt_orchestrator/config/__init__.py +++ b/prompt_orchestrator/config/__init__.py @@ -1,11 +1,13 @@ from .config_store import ConfigStore from .module_config import ModuleConfig -from .prompt_config import PromptConfig +from .prompt_config import OutputContractConfig, PromptConfig, ToolCallingPolicyConfig from .settings import OrchestratorSettings __all__ = [ "ConfigStore", "ModuleConfig", + "OutputContractConfig", "OrchestratorSettings", "PromptConfig", + "ToolCallingPolicyConfig", ] diff --git a/prompt_orchestrator/config/prompt_config.py b/prompt_orchestrator/config/prompt_config.py index ffe430f..7e8c6ec 100644 --- a/prompt_orchestrator/config/prompt_config.py +++ b/prompt_orchestrator/config/prompt_config.py @@ -1,8 +1,27 @@ from __future__ import annotations +from typing import Literal + from pydantic import BaseModel, Field +class OutputContractConfig(BaseModel): + mode: Literal["json_markdown", "json", "markdown", "text"] = "json_markdown" + strict: bool = True + schema_hint: str = ( + '{"summary": "str", "findings": ["str"], ' + '"risks": ["str"], "actions": ["str"], "citations": ["str"]}' + ) + + +class ToolCallingPolicyConfig(BaseModel): + mode: Literal["allow", "deny", "allowlist"] = "allow" + max_calls: int = 8 + allowed_tools: list[str] = Field(default_factory=list) + require_json_arguments: bool = True + require_tool_result_ack: bool = True + + class PromptConfig(BaseModel): system_prompt: str role: str @@ -10,10 +29,41 @@ class PromptConfig(BaseModel): constraints: list[str] = Field(default_factory=list) output_format: str examples: list[str] = Field(default_factory=list) + response_language: Literal["ru", "en", "auto"] = "ru" + output_contract: OutputContractConfig = Field(default_factory=OutputContractConfig) + tool_calling_policy: ToolCallingPolicyConfig = Field(default_factory=ToolCallingPolicyConfig) + + def _render_language_instruction(self) -> str: + if self.response_language == "ru": + return "Russian (ru)" + if self.response_language == "en": + return "English (en)" + return "Auto-detect from user query (auto)" + + def _render_output_contract(self) -> str: + strict = "strict" if self.output_contract.strict else "soft" + return ( + f"mode={self.output_contract.mode}; " + f"enforcement={strict}; " + f"schema={self.output_contract.schema_hint}" + ) + + def _render_tool_policy(self) -> str: + allowed = ", ".join(self.tool_calling_policy.allowed_tools) or "Any tool" + return ( + f"mode={self.tool_calling_policy.mode}; " + f"max_calls={self.tool_calling_policy.max_calls}; " + f"allowed_tools={allowed}; " + f"json_args={self.tool_calling_policy.require_json_arguments}; " + f"ack_result={self.tool_calling_policy.require_tool_result_ack}" + ) def render_static_header(self, include_header: bool = False) -> str: constraints = "\n".join(f"- {item}" for item in self.constraints) or "- None" examples = "\n\n".join(self.examples) or "None" + response_language = self._render_language_instruction() + output_contract = self._render_output_contract() + tool_policy = self._render_tool_policy() header = "=== STATIC PART (CACHE-FRIENDLY) ===\n" if include_header else "" return ( f"{header}" @@ -25,8 +75,14 @@ def render_static_header(self, include_header: bool = False) -> str: f"{self.task}\n\n" "Constraints:\n" f"{constraints}\n\n" + "Response Language:\n" + f"{response_language}\n\n" "Output Format:\n" f"{self.output_format}\n\n" + "Output Contract:\n" + f"{output_contract}\n\n" + "Tool Calling Policy:\n" + f"{tool_policy}\n\n" "Examples:\n" f"{examples}" ) diff --git a/pyproject.toml b/pyproject.toml index 3dc31fe..4b3caa9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "prompt-orchestrator" -version = "0.1.7" +version = "0.1.9" description = "Structured prompt orchestration with cache, safety, and analyzer layers" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.10" diff --git a/tests/test_core_behaviors.py b/tests/test_core_behaviors.py index 28aa63e..e2cfe6f 100644 --- a/tests/test_core_behaviors.py +++ b/tests/test_core_behaviors.py @@ -4,8 +4,8 @@ from prompt_orchestrator import ( LocalTTLCacheBackend, - NoRAGProvider, OllamaSummaryClient, + OutputContractConfig, OrchestratorSettings, PromptConfig, PromptContextManager, @@ -15,6 +15,7 @@ SummaryLLMConfig, TokenCounter, SafetyLLMConfig, + ToolCallingPolicyConfig, ) from prompt_orchestrator.safety import llm as safety_llm_module from prompt_orchestrator.context.state import DocChunk @@ -66,6 +67,49 @@ def _base_config() -> PromptConfig: ) +def test_prompt_config_enterprise_defaults() -> None: + cfg = _base_config() + + assert cfg.response_language == "ru" + assert cfg.output_contract.strict is True + assert cfg.output_contract.mode == "json_markdown" + assert cfg.tool_calling_policy.mode == "allow" + assert cfg.tool_calling_policy.max_calls == 8 + + +def test_prompt_config_renders_language_output_and_tool_policy() -> None: + cfg = PromptConfig( + system_prompt="You are a helpful assistant.", + role="Engineer", + task="Answer questions clearly.", + constraints=["No hallucinations"], + output_format="Markdown", + examples=["Q: hi A: hello"], + response_language="ru", + output_contract=OutputContractConfig( + mode="json", + strict=True, + schema_hint='{"answer": "str", "citations": ["str"]}', + ), + tool_calling_policy=ToolCallingPolicyConfig( + mode="allowlist", + max_calls=4, + allowed_tools=["retrieve_context", "build_attribution"], + require_json_arguments=True, + require_tool_result_ack=True, + ), + ) + + rendered = cfg.render_static_header(include_header=True) + assert "Response Language:" in rendered + assert "Russian (ru)" in rendered + assert "Output Contract:" in rendered + assert "mode=json; enforcement=strict" in rendered + assert "Tool Calling Policy:" in rendered + assert "mode=allowlist; max_calls=4" in rendered + assert "allowed_tools=retrieve_context, build_attribution" in rendered + + def test_local_ttl_cache_expires_items() -> None: cache = LocalTTLCacheBackend(default_ttl_seconds=1) cache.set("k", {"v": 1})