oclaw/runtime/system_prompt.py
oliver 25180921e7 兼容 memory-wiki 输入路径并强调相对路径
- memory-wiki 的 wiki_root 路径解析兼容:相对路径 / data/wiki 前缀 / wiki_root 下绝对路径
- 系统提示词强调:模型传相对 wiki 根路径(相对 data/wiki),避免直接给 data/wiki/... 或绝对路径
- 修复并回归:memory-wiki 工具动态加载模块 import-time 错误(sys.modules 注入)
- chat.js 增强会话滚动跟随(异步渲染补滚)

Made-with: Cursor
2026-04-29 08:54:58 +08:00

170 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import threading
from typing import Any
from oclaw.runtime.memory_stage import render_memory_context_block
from oclaw.runtime.project_context_prompt import build_project_context_block
from oclaw.runtime.skills_prompt import build_skills_catalog_block
from oclaw.runtime.types import OclawMemoryContext
from oclaw.runtime.workspaces.experts import expert_workspace_signature_token
from oclaw.prompts.loader import render_runtime_prompt
from oclaw.runtime.tools.base import ToolRegistry
_EXECUTOR_STATIC_PROMPT_CACHE_LOCK = threading.Lock()
_EXECUTOR_STATIC_PROMPT_CACHE: dict[tuple[Any, ...], str] = {}
def _executor_prompt_settings_signature(store: Any) -> tuple[str, ...]:
keys = (
"AIA_SKILL_RUNTIME_ENABLED",
"AIA_SKILLS_PROMPT_IN_SYSTEM",
"AIA_SKILL_DISABLED_NAMES",
"AIA_SKILL_ROLE_BINDING_ENABLED",
"AIA_SKILL_ROLE_BINDING_MANAGER_INHERIT",
"AIA_PROJECT_CONTEXT_MAX_FILE_CHARS",
"AIA_PROJECT_CONTEXT_MAX_TOTAL_CHARS",
)
parts: list[str] = []
for key in keys:
try:
val = str(store.get_setting(key) or "")
except Exception:
val = ""
parts.append(f"{key}={val}")
return tuple(parts)
def _unified_skill_policy_guidance() -> str:
# Global policy for all agents (including dynamic/ephemeral) — appended to base_system in build_executor_system_prompt.
return (
"- 如果用户问你有哪些技能(skill/技能),请直接根据已注入的技能目录及其 description 回答。\n"
"- 不要为了“列出技能”而去读取 SKILL.md。只有在你确实需要某个技能的详细使用说明时,才读取对应 SKILL.md。\n"
"- 当你需要技能细节时,请按目录中给出的 path 读取对应的 SKILL.md。\n"
"- 当对话涉及长期记忆、用户身份/偏好、项目背景延续、复发问题沉淀时,优先启用 wiki-first-autonomy 技能,并优先使用 memory_wiki_search/memory_wiki_get 检索上下文,再执行与回复。\n"
"- memory wiki 请仅传相对 wiki 根的路径(相对 `data/wiki`),例如 `improvement/learnings.md`;不要传 `data/wiki/...` 或绝对路径。\n"
"- 当新增事实会影响后续决策时,完成当前任务后使用 memory_wiki_apply 写入结构化记忆,并用 memory_wiki_lint 做质量检查。\n"
"- 技能包由说明文档和可选文件组成。运行时不会自动执行技能 `scripts/` 目录下的文件;\n"
"- internal hooks 是独立系统,也不会自动执行这些脚本。\n"
"- 当用户明确要求运行技能脚本时,请使用项目允许的 terminal/bash/exec(或同类)工具执行;\n"
"- 如果脚本依赖相对路径(例如 `.learnings/`),请将工作目录设置为用户工作区。\n"
"- 在没有显式工具调用成功结果前,不要假设脚本已经执行。\n"
"- 在 Windows 上,`.sh` 可能需要 Git Bash、WSL 或等效环境。\n"
)
def build_executor_system_prompt(
*,
store: Any,
tools: ToolRegistry | None,
base_url: str,
base_system: str,
memory_context: OclawMemoryContext | None,
lang: str,
workspace_dir: str | None = None,
skill_binding_role: str | None = None,
) -> str:
"""Build the final system string for the oclaw executor (memory block + skills catalog).
`lang` is reserved for future localized system fragments; current templates are bilingual/static.
"""
_ = lang # reserved for i18n extensions
final_system = get_executor_prompt_static(
store=store,
tools=tools,
base_url=base_url,
base_system=base_system,
workspace_dir=workspace_dir,
skill_binding_role=skill_binding_role,
)
mem_block = render_memory_context_block(memory_context or OclawMemoryContext())
if not mem_block:
return final_system
return render_runtime_prompt(
"runtime/system_with_memory.md",
variables={"system_prompt": final_system, "memory_context": mem_block},
strict=True,
)
def get_executor_prompt_static(
*,
store: Any,
tools: ToolRegistry | None,
base_url: str,
base_system: str,
workspace_dir: str | None = None,
skill_binding_role: str | None = None,
) -> str:
cache_key = (
str(base_url or "").strip(),
str(base_system or "").strip(),
str(workspace_dir or "").strip(),
str(skill_binding_role or "").strip().lower(),
expert_workspace_signature_token(),
_executor_prompt_settings_signature(store),
bool(tools is not None),
)
with _EXECUTOR_STATIC_PROMPT_CACHE_LOCK:
cached = _EXECUTOR_STATIC_PROMPT_CACHE.get(cache_key)
if isinstance(cached, str):
return cached
final_system = str(base_system or "").strip()
guide = _unified_skill_policy_guidance().strip()
if guide and guide not in final_system:
final_system = f"{final_system}\n\n{guide}".strip()
project_block = build_project_context_block(store=store, workspace_dir=workspace_dir)
if project_block:
final_system = f"{final_system}\n\n{project_block}".strip()
if tools is not None:
cat = build_skills_catalog_block(
store=store,
registry=tools,
base_url=str(base_url or ""),
skill_binding_role=skill_binding_role,
)
if cat.strip():
final_system = render_runtime_prompt(
"runtime/system_with_skills.md",
variables={"system_body": final_system, "skills_catalog": cat},
strict=True,
)
with _EXECUTOR_STATIC_PROMPT_CACHE_LOCK:
_EXECUTOR_STATIC_PROMPT_CACHE[cache_key] = final_system
if len(_EXECUTOR_STATIC_PROMPT_CACHE) > 256:
_EXECUTOR_STATIC_PROMPT_CACHE.clear()
return final_system
def warm_executor_prompt_cache(
*,
store: Any,
tools: ToolRegistry | None,
base_url: str,
role_base_systems: dict[str, str],
workspace_dir: str | None = None,
) -> dict[str, int]:
warmed = 0
for role, base_system in (role_base_systems or {}).items():
_ = get_executor_prompt_static(
store=store,
tools=tools,
base_url=base_url,
base_system=str(base_system or ""),
workspace_dir=workspace_dir,
skill_binding_role=str(role or "").strip().lower() or None,
)
warmed += 1
return {"roles_warmed": int(warmed)}
def build_oclaw_executor_system_prompt(**kwargs: Any) -> str:
return build_executor_system_prompt(**kwargs)
__all__ = [
"build_executor_system_prompt",
"build_oclaw_executor_system_prompt",
"get_executor_prompt_static",
"warm_executor_prompt_cache",
]