Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions config/plugins.thought-aligner.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"phases": {
"llm_before": {
"client": [],
"server": []
},
"llm_after": {
"client": [],
"server": [
{
"name": "thought_aligner",
"env": {
"base_url": "$THOUGHT_ALIGNER_BASE_URL",
"api_key": "$THOUGHT_ALIGNER_API_KEY",
"model": "$THOUGHT_ALIGNER_MODEL"
},
"kwargs": {
"timeout_s": 30,
"failure_mode": "deny",
"max_history_items": 8,
"max_instruction_chars": 12000,
"max_thought_chars": 8000,
"max_observation_chars": 12000
}
}
]
},
"tool_before": {
"client": [],
"server": [
{
"name": "rule_based_plugin",
"env": {}
}
]
},
"tool_after": {
"client": [],
"server": []
}
}
}
1 change: 1 addition & 0 deletions docs/en/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* [AgentGuard Plugins](plugins.md)
* [Builtin Plugins](plugins/builtin_plugins.md)
* [rule_based_plugin](plugins/rule_based_plugin.md)
* [Thought-Aligner](plugins/thought_aligner.md)
* [Visual Policy Configuration](policies/quick_config.md)
* [Policy DSL Structure](policies/dsl_basic_structure.md)
* [jailbreak_check](plugins/jailbreak_check.md)
Expand Down
1 change: 1 addition & 0 deletions docs/en/plugins/builtin_plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ AgentGuard includes built-in plugins for common runtime protection needs. This p

- [rule_based_plugin](rule_based_plugin.md): a server-only rule engine for tool-call access control and policy evaluation that can either return fixed `ALLOW` / `DENY` decisions or escalate matched cases to `HUMAN_CHECK` / `LLM_CHECK`.
- [jailbreak_check](jailbreak_check.md): the same LLM-input detector can also run on the AgentGuard server when you want centralized governance and auditing.
- [Thought-Aligner](thought_aligner.md): an opt-in `llm_after` intervention that rewrites exposed reasoning on the server and makes a compatible Python client regenerate its action before execution.

Other utility plugins also exist in the codebase, but this page focuses on the built-in plugins that are documented for direct use in the public docs.
2 changes: 1 addition & 1 deletion docs/en/plugins/custom_server_plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ Server plugin specs are read from the `server` list in `config/plugins.json` or
- `name`: registered plugin name.
- `class` or `plugin`: optional import-path alternatives to `name`.
- The current server runtime resolves plugin classes by `name` or import path.
- Extra fields may remain in stored config, but the current server plugin manager does not inject `env` or `kwargs` into server plugin constructors.
- `kwargs` values are passed to the plugin constructor. `env` maps constructor attribute names to environment-variable references such as `$MY_PLUGIN_API_KEY`; references are resolved in the server process.

## Output

Expand Down
78 changes: 78 additions & 0 deletions docs/en/plugins/thought_aligner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Thought-Aligner

`thought_aligner` is an opt-in `llm_after` server plugin. It holds a Python agent's first model response, sends the exposed reasoning to a server-hosted Thought-Aligner endpoint, and instructs the same agent model to regenerate `Action` and `Action Input` from the aligned thought before the first action can reach the framework's parser or tool executor.

The implementation follows the [Thought-Aligner-7B model-card prompt](https://huggingface.co/WhitzardAgent/Thought-Aligner-7B). Review that model's CC BY-NC 4.0 license before commercial use.

## Execution flow

1. The patched Python client calls the original agent model but keeps its response inside the wrapper.
2. The client normalizes the LLM output and tells the server whether that concrete request shape can be safely regenerated.
3. The server builds:
- the user instruction, preferring explicit metadata and otherwise reading user/human messages or rendered prompts;
- the current thought, preferring explicit normalized fields, then common reasoning aliases, thought tags, and ReAct `Thought:` text;
- completed earlier thought/tool-result pairs, formatted as `<thought> ... </thought>` and `<observation> ... </observation>`.
4. If Thought-Aligner changes the thought, the server returns `align_thought` with the aligned thought.
5. The client copies the original request, injects the aligned thought plus an action-only instruction, calls the original model once more, and returns only the regenerated result. The retry is marked and cannot trigger another alignment loop.

The Thought-Aligner model call and its credentials stay on the AgentGuard server. The original agent model remains on the client side because only the client owns the native model callable and framework-specific request/response objects.

## Configuration

Set dedicated server environment variables. Do not put a key in JSON or commit it to the repository.

```bash
export THOUGHT_ALIGNER_BASE_URL="https://your-thought-aligner-host/v1"
export THOUGHT_ALIGNER_API_KEY="replace-with-a-secret"
export THOUGHT_ALIGNER_MODEL="thought-aligner-7b"
export AGENTGUARD_SERVER_PLUGIN_CONFIG="./config/plugins.thought-aligner.example.json"
```

Then start the AgentGuard server normally. The example file enables the plugin only in the server-side `llm_after` phase; the regular `config/plugins.json` remains unchanged, so Thought-Aligner is disabled by default.

The Python client needs a server URL and a decision timeout longer than the server plugin's model timeout:

```python
from agentguard import AgentGuard

guard = AgentGuard(
"agent-session",
server_url="http://127.0.0.1:8000",
remote_timeout_s=45,
remote_retries=0,
)
guard.attach_langchain(agent)
```

If a framework passes an opaque prompt and the server cannot reliably recover the original user task, provide it explicitly before the guarded turn:

```python
guard.context.metadata["instruction"] = user_instruction
```

Relevant plugin options:

- `timeout_s`: Thought-Aligner endpoint timeout; default `30` seconds.
- `failure_mode`: `deny` by default, which withholds the first action if an attempted alignment fails. `allow` preserves availability but releases the original response after a model failure.
- `max_history_items`: maximum completed thought/observation pairs; default `8`.
- `max_instruction_chars`, `max_thought_chars`, `max_observation_chars`: per-field bounds before the external model call.

## Supported input and output forms

Thought extraction is independent of framework classes and recognizes:

- normalized `thought`;
- nested `reasoning_content`, `reasoning`, `thinking`, `plan`, and `analysis` fields;
- `<think>`, `<thought>`, `<reason>`, `<reasoning>`, and `<analysis>` blocks;
- ReAct-style `Thought:` content before `Action`, `Action Input`, `Observation`, or `Final Answer`.

Python regeneration currently supports non-streaming, concrete patched LLM calls whose request is a string, a dict/list/tuple of chat messages, an `input`/`prompt`/`messages` argument, or a LangChain-style `agent_scratchpad`. Common string, dictionary, and Pydantic message responses retain their native shape where possible.

## Safety and compatibility boundaries

- A provider that does not expose reasoning gives AgentGuard no truthful Thought to align. In that case the plugin is a no-op; it never invents hidden chain-of-thought.
- A thought-bearing call from an old or unsupported client is denied before the Thought-Aligner call because that client cannot prove it will regenerate the action.
- Streaming agents that emit action-bearing chunks before `llm_after`, including current streaming-specific integrations, are not covered by this interception path. Buffer the complete turn or add a framework-native pre-action hook before enabling the plugin.
- The implementation performs at most one regeneration. A second `align_thought` directive is blocked.
- Instruction, exposed thought, and selected observations leave the AgentGuard server for the configured model endpoint. Apply your data-retention, redaction, and regional-processing requirements to that endpoint. The server decision metadata contains the aligned thought but does not copy the original instruction or unsafe thought into the decision.
- Thought alignment complements, but does not replace, AgentGuard tool policies. The regenerated action still passes through the existing `tool_before` policy path.
1 change: 1 addition & 0 deletions docs/zh/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* [AgentGuard插件](plugins.md)
* [内置插件](plugins/builtin_plugins.md)
* [rule_based_plugin](plugins/rule_based_plugin.md)
* [Thought-Aligner](plugins/thought_aligner.md)
* [可视化策略配置](policies/quick_config.md)
* [策略 DSL 基本结构](policies/dsl_basic_structure.md)
* [jailbreak_check](plugins/jailbreak_check.md)
Expand Down
1 change: 1 addition & 0 deletions docs/zh/plugins/builtin_plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ AgentGuard 提供了一组面向常见运行时防护需求的内置 plugin。

- [rule_based_plugin](rule_based_plugin.md):一个仅运行在 server 侧的规则引擎,用于工具调用访问控制和策略评估;它既可以直接返回固定的 `ALLOW` / `DENY`,也可以把命中的情况转入 `HUMAN_CHECK` / `LLM_CHECK`。
- [jailbreak_check](jailbreak_check.md):同一个 LLM 输入检测器也可以部署在 AgentGuard Server 侧,用于集中式治理和审计。
- [Thought-Aligner](thought_aligner.md):一个按需启用的 `llm_after` 防御,在 server 侧改写可获得的推理,并让兼容的 Python client 在执行前重新生成 Action。

代码库里还有其他工具型 plugin,但这个页面当前聚焦于已经在公开文档中单独展开说明的内置 plugin。
2 changes: 1 addition & 1 deletion docs/zh/plugins/custom_server_plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ Server plugin spec 从 `config/plugins.json` 或运行时 plugin config 的 `ser
- `name`:注册后的 plugin 名称。
- `class` 或 `plugin`:也可以作为 `name` 的替代形式,用来写导入路径。
- 当前 server runtime 会按 `name` 或导入路径解析 plugin 类。
- 额外字段会保留在配置中,但当前 server plugin manager 不会把 `env` 或 `kwargs` 注入 server plugin 构造函数
- `kwargs` 的值会传给 plugin 构造函数;`env` 可以把构造参数名映射为 `$MY_PLUGIN_API_KEY` 这类环境变量引用,并由 server 进程解析

## 输出

Expand Down
78 changes: 78 additions & 0 deletions docs/zh/plugins/thought_aligner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Thought-Aligner

`thought_aligner` 是一个默认关闭、按需启用的 server 侧 `llm_after` plugin。它会先扣住 Python agent 第一次生成的模型响应,把其中可获得的推理发送到 server 上配置的 Thought-Aligner 端点,然后让同一个 agent 模型基于安全 Thought 重新生成 `Action` 和 `Action Input`。第一次生成的 Action 不会先交给框架解析器或工具执行器。

实现遵循 [Thought-Aligner-7B 模型卡](https://huggingface.co/WhitzardAgent/Thought-Aligner-7B)给出的 prompt。商业使用前请确认该模型的 CC BY-NC 4.0 许可证是否适用。

## 执行流程

1. Python client 调用原 agent 模型,但 wrapper 暂不向 agent 返回第一次响应。
2. Client 标准化 LLM 输出,并告诉 server 当前这次请求的数据形态是否能安全回跳。
3. Server 构造:
- 用户指令:优先使用显式 metadata,否则从 user/human 消息、嵌套输入或渲染后的 prompt 中提取;
- 当前 Thought:依次尝试标准字段、常见 reasoning 别名、Thought 标签和 ReAct `Thought:` 文本;
- 之前已经完成的 Thought/工具结果对,并用 `<thought> ... </thought>`、`<observation> ... </observation>` 标记。
4. Thought-Aligner 改写了 Thought 时,server 返回带安全 Thought 的 `align_thought` 决策。
5. Client 复制原请求,注入安全 Thought 和“只生成 Action/Final Answer”的指令,再调用一次原模型,只把重新生成的结果返回给 agent。重试事件会被标记,不会再次进入对齐循环。

Thought-Aligner 调用及其凭证始终在 AgentGuard server 侧。原 agent 模型仍由 client 调用,因为只有 client 持有原生模型 callable 以及具体框架的请求、响应对象。

## 配置

在 server 进程中设置独立环境变量。不要把密钥直接写入 JSON,也不要提交到仓库。

```bash
export THOUGHT_ALIGNER_BASE_URL="https://your-thought-aligner-host/v1"
export THOUGHT_ALIGNER_API_KEY="replace-with-a-secret"
export THOUGHT_ALIGNER_MODEL="thought-aligner-7b"
export AGENTGUARD_SERVER_PLUGIN_CONFIG="./config/plugins.thought-aligner.example.json"
```

然后正常启动 AgentGuard server。示例配置只在 server 的 `llm_after` 阶段启用该 plugin;现有 `config/plugins.json` 不变,所以默认行为不会开启 Thought-Aligner。

Python client 需要配置 server 地址,并确保远程决策超时大于 server plugin 的模型超时:

```python
from agentguard import AgentGuard

guard = AgentGuard(
"agent-session",
server_url="http://127.0.0.1:8000",
remote_timeout_s=45,
remote_retries=0,
)
guard.attach_langchain(agent)
```

如果某个框架传入的是不透明 prompt,server 无法稳定还原最初的用户任务,可以在受保护轮次开始前显式设置:

```python
guard.context.metadata["instruction"] = user_instruction
```

主要 plugin 参数:

- `timeout_s`:Thought-Aligner 端点超时,默认 `30` 秒。
- `failure_mode`:默认 `deny`;已经进入对齐的请求如果模型调用失败,会扣住第一次 Action。设置为 `allow` 可优先保证可用性,但模型失败后会释放原响应。
- `max_history_items`:最多传入的已完成 Thought/Observation 对,默认 `8`。
- `max_instruction_chars`、`max_thought_chars`、`max_observation_chars`:发送给外部模型前各字段的长度上限。

## 支持的输入与输出形式

Thought 提取不依赖具体框架类,目前识别:

- 标准化后的 `thought`;
- 嵌套的 `reasoning_content`、`reasoning`、`thinking`、`plan`、`analysis` 字段;
- `<think>`、`<thought>`、`<reason>`、`<reasoning>`、`<analysis>` 块;
- ReAct 文本中位于 `Action`、`Action Input`、`Observation` 或 `Final Answer` 之前的 `Thought:`。

Python 回跳当前支持非流式、已经被具体 patch 的 LLM 调用,请求形态可以是字符串、消息 dict/list/tuple、`input`/`prompt`/`messages` 参数,或 LangChain 风格 `agent_scratchpad`。对于常见字符串、字典和 Pydantic 消息响应,会尽量保留原生返回类型。

## 安全与兼容边界

- 如果模型供应商完全不暴露推理,AgentGuard 就没有真实 Thought 可供对齐。此时 plugin 不执行改写,也不会猜测或伪造隐藏思维链。
- 如果旧 client 或不支持回跳的 client 发送了包含 Thought 的事件,server 会在调用 Thought-Aligner 前拒绝该轮,因为无法证明 client 会重新生成 Action。
- 如果流式 agent 在 `llm_after` 之前就把包含 Action 的 chunk 发出,当前拦截路径无法提供保护。启用前需要缓冲完整一轮,或增加框架原生的 Action 前 hook。
- 每轮最多回跳一次;第二个 `align_thought` 决策会被阻断。
- 用户指令、可见 Thought 和选中的 Observation 会离开 AgentGuard server,发送到配置的模型端点。需要对该端点落实数据保留、脱敏和地域处理要求。server 的最终 decision metadata 只携带安全 Thought,不会再次复制原指令或不安全 Thought。
- Thought 对齐不能替代工具策略。重新生成的 Action 仍会经过 AgentGuard 原有的 `tool_before` 策略链。
29 changes: 27 additions & 2 deletions src/client/python/agentguard/adapters/agent/langchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,15 @@ def _normalize_langchain_request(

def _normalize_langchain_llm_output(value: Any) -> Any:
normalized = _normalize_langchain_value(value)
if isinstance(normalized, str):
parsed = _parse_tagged_llm_output(normalized)
if parsed.thought is None:
return normalized
return {
"output": normalized,
"thought": parsed.thought,
"final_output": parsed.final_output,
}
return _extract_langchain_llm_output_fields(normalized)


Expand Down Expand Up @@ -419,7 +428,7 @@ def _first_non_empty_text(value: dict[str, Any], *keys: str) -> str | None:
@dataclass(frozen=True)
class _ParsedLLMOutput:
thought: str | None
final_output: str
final_output: str | None


_THOUGHT_TAG_RE = re.compile(
Expand All @@ -430,12 +439,28 @@ class _ParsedLLMOutput:
r"<(?P<tag>answer|final|final_output)\b[^>]*>(?P<body>.*?)</(?P=tag)>",
flags=re.IGNORECASE | re.DOTALL,
)
_REACT_THOUGHT_RE = re.compile(
r"(?:^|\n)\s*(?:Thought|Reasoning|Analysis|思考)\s*:\s*(?P<body>.*?)"
r"(?=\n\s*(?:Action(?:\s+Input)?|Observation|Final\s+Answer|Answer|"
r"行动|观察|最终答案)\s*:|\Z)",
flags=re.IGNORECASE | re.DOTALL,
)


def _parse_tagged_llm_output(output: str) -> _ParsedLLMOutput:
thought_matches = list(_THOUGHT_TAG_RE.finditer(output))
if not thought_matches:
return _ParsedLLMOutput(thought=None, final_output=output)
react_match = _REACT_THOUGHT_RE.search(output)
if react_match is None:
return _ParsedLLMOutput(thought=None, final_output=output)
thought = react_match.group("body").strip() or None
remainder = f"{output[:react_match.start()]}{output[react_match.end():]}".strip()
final_output = (
None
if re.match(r"^\s*Action\s*:", remainder, flags=re.IGNORECASE)
else remainder
)
return _ParsedLLMOutput(thought=thought, final_output=final_output)

thought_parts = [match.group("body").strip() for match in thought_matches]
thought = "\n\n".join(part for part in thought_parts if part) or None
Expand Down
Loading
Loading