Files
NovelWriter_public/agent_tools.py
NovelWriter c8f0609375 feat: 同步多项功能更新(创意讨论模式/润色模式/细纲生成/场景分段/用户画像/网络代理/Agent工具)
创意讨论:5种讨论模式(轻松闲聊/专业顾问/头脑风暴/魔鬼代言人/角色扮演)
润色功能:3种模式(通用/修改剧情/增加内容)+ 4项可选上下文注入
章节细纲:新增独立的 detailedOutline 生成步骤,支持 concise/detailed 两种模式
按场景分段生成:基于细纲解析场景,高强度场景与普通场景分别用不同密度生成
用户画像:全局偏好画像(独立页面),半自动从修订/润色/讨论中提取偏好
网络代理:模型配置页加代理设置区块,支持 LLM/Embedding 全局代理
导出标题去重:检测章节自带标题避免重复
Agent 配置:agent_tools.py 工具定义 + agent_config.py system prompt(已清理R-18引用)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 12:21:40 +08:00

669 lines
30 KiB
Python
Raw Permalink 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.
# agent_tools.py
# -*- coding: utf-8 -*-
"""
NovelWriter Agent Tool Schema 定义
将 NovelWriter 的核心 API 封装为 LLM function calling / tool use 的工具定义。
支持 OpenAI、Claude、Gemini 等主流 LLM 的 tool use 格式。
"""
AGENT_TOOLS = [
# ═══════════════════════════════════════════════════
# 项目管理
# ═══════════════════════════════════════════════════
{
"name": "list_projects",
"description": "列出所有小说项目及当前激活的项目名",
"api": {"method": "GET", "path": "/projects"},
"parameters": {},
"streaming": False,
},
{
"name": "create_project",
"description": "创建一个新的小说项目",
"api": {"method": "POST", "path": "/projects"},
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "项目名称"},
"filepath": {"type": "string", "description": "项目文件路径,如 ./output/我的小说"},
},
"required": ["name"],
},
"streaming": False,
},
{
"name": "activate_project",
"description": "切换到指定的小说项目",
"api": {"method": "POST", "path": "/projects/{name}/activate"},
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "要激活的项目名称"},
},
"required": ["name"],
},
"streaming": False,
},
{
"name": "update_project",
"description": "更新项目的元数据主题、类型、章节数、字数、XP类型等",
"api": {"method": "PUT", "path": "/projects/{name}"},
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "项目名称"},
"topic": {"type": "string", "description": "小说主题/简介"},
"genre": {"type": "string", "description": "小说类型(如:玄幻、都市、科幻)"},
"num_chapters": {"type": "integer", "description": "总章节数"},
"word_number": {"type": "integer", "description": "每章字数"},
"user_guidance": {"type": "string", "description": "创作指导/要求"},
"xp_type": {"type": "string", "description": "XP类型/核心玩法描述"},
},
"required": ["name"],
},
"streaming": False,
},
# ═══════════════════════════════════════════════════
# 架构生成(分步)
# ═══════════════════════════════════════════════════
{
"name": "generate_core_seed",
"description": "根据主题、类型等信息,生成小说的核心种子(故事核心概念/前提)",
"api": {"method": "POST", "path": "/generate/architecture/step/core_seed"},
"parameters": {
"type": "object",
"properties": {
"topic": {"type": "string", "description": "小说主题/简介"},
"genre": {"type": "string", "description": "小说类型"},
"num_chapters": {"type": "integer", "description": "总章节数"},
"word_number": {"type": "integer", "description": "每章字数"},
"step_guidance": {"type": "string", "description": "本步骤的额外指导"},
"xp_type": {"type": "string", "description": "XP类型/核心玩法"},
},
"required": ["topic", "genre"],
},
"streaming": True,
},
{
"name": "generate_characters",
"description": "基于核心种子,生成角色动力学(角色设定、关系网)",
"api": {"method": "POST", "path": "/generate/architecture/step/characters"},
"parameters": {
"type": "object",
"properties": {
"seed_text": {"type": "string", "description": "核心种子文本"},
"step_guidance": {"type": "string", "description": "角色设计的额外指导"},
"num_characters": {"type": "string", "description": "角色数量,如 '3-5'"},
"xp_type": {"type": "string", "description": "XP类型"},
},
"required": ["seed_text"],
},
"streaming": True,
},
{
"name": "generate_world_building",
"description": "基于核心种子和角色,生成世界观设定",
"api": {"method": "POST", "path": "/generate/architecture/step/world"},
"parameters": {
"type": "object",
"properties": {
"seed_text": {"type": "string", "description": "核心种子文本"},
"char_text": {"type": "string", "description": "角色动力学文本(可选,用于参考)"},
"step_guidance": {"type": "string", "description": "世界观设计的额外指导"},
"xp_type": {"type": "string", "description": "XP类型"},
},
"required": ["seed_text"],
},
"streaming": True,
},
{
"name": "generate_plot",
"description": "基于核心种子、角色、世界观,生成情节架构",
"api": {"method": "POST", "path": "/generate/architecture/step/plot"},
"parameters": {
"type": "object",
"properties": {
"seed_text": {"type": "string", "description": "核心种子文本"},
"char_text": {"type": "string", "description": "角色动力学文本"},
"world_text": {"type": "string", "description": "世界观文本"},
"step_guidance": {"type": "string", "description": "情节设计的额外指导"},
"num_chapters": {"type": "integer", "description": "总章节数"},
"xp_type": {"type": "string", "description": "XP类型"},
},
"required": ["seed_text", "char_text", "world_text"],
},
"streaming": True,
},
{
"name": "generate_char_state",
"description": "基于角色动力学,生成角色初始状态(树状结构的角色属性)",
"api": {"method": "POST", "path": "/generate/architecture/step/char_state"},
"parameters": {
"type": "object",
"properties": {
"char_dynamics_text": {"type": "string", "description": "角色动力学文本"},
},
"required": ["char_dynamics_text"],
},
"streaming": True,
},
{
"name": "assemble_architecture",
"description": "将分步生成的各组件(种子、角色、世界观、情节、角色状态)组装为完整架构并保存",
"api": {"method": "POST", "path": "/generate/architecture/assemble"},
"parameters": {
"type": "object",
"properties": {
"seed_text": {"type": "string", "description": "核心种子文本"},
"char_text": {"type": "string", "description": "角色动力学文本"},
"char_state_text": {"type": "string", "description": "角色状态文本"},
"world_text": {"type": "string", "description": "世界观文本"},
"plot_text": {"type": "string", "description": "情节架构文本"},
"topic": {"type": "string", "description": "主题"},
"genre": {"type": "string", "description": "类型"},
"num_chapters": {"type": "integer", "description": "章节数"},
"word_number": {"type": "integer", "description": "每章字数"},
},
"required": ["seed_text", "char_text", "world_text", "plot_text"],
},
"streaming": False,
},
# ═══════════════════════════════════════════════════
# 修订
# ═══════════════════════════════════════════════════
{
"name": "revise_content",
"description": "根据用户的修改建议,修订架构中的某个组件(核心种子/角色/世界观/情节/角色状态)",
"api": {"method": "POST", "path": "/generate/architecture/step/revise"},
"parameters": {
"type": "object",
"properties": {
"original_content": {"type": "string", "description": "待修订的原始内容"},
"revision_guidance": {"type": "string", "description": "用户的修改建议"},
"step_type": {
"type": "string",
"description": "修订的组件类型",
"enum": ["core_seed", "characters", "char_state", "world", "plot"],
},
"include_core_seed": {"type": "boolean", "description": "是否注入核心种子作为参考"},
"include_characters": {"type": "boolean", "description": "是否注入角色信息作为参考"},
"include_world_building": {"type": "boolean", "description": "是否注入世界观作为参考"},
"include_plot": {"type": "boolean", "description": "是否注入情节架构作为参考"},
},
"required": ["original_content", "revision_guidance", "step_type"],
},
"streaming": True,
},
# ═══════════════════════════════════════════════════
# 蓝图与细纲
# ═══════════════════════════════════════════════════
{
"name": "generate_blueprint",
"description": "基于完整架构,生成章节蓝图(每章的定位、悬念、伏笔、强度等)",
"api": {"method": "POST", "path": "/generate/blueprint"},
"parameters": {
"type": "object",
"properties": {
"num_chapters": {"type": "integer", "description": "总章节数"},
"user_guidance": {"type": "string", "description": "蓝图生成的额外指导"},
"xp_type": {"type": "string", "description": "XP类型"},
},
"required": [],
},
"streaming": True,
},
{
"name": "generate_detailed_outline",
"description": "基于架构和蓝图,生成指定章节范围的详细细纲(场景分解、节奏、对话要点)",
"api": {"method": "POST", "path": "/generate/detailed_outline"},
"parameters": {
"type": "object",
"properties": {
"start_chapter": {"type": "integer", "description": "起始章节号"},
"end_chapter": {"type": "integer", "description": "结束章节号"},
"num_chapters": {"type": "integer", "description": "总章节数"},
"user_guidance": {"type": "string", "description": "细纲生成的额外指导"},
"xp_type": {"type": "string", "description": "XP类型"},
"outline_mode": {
"type": "string",
"description": "细纲模式concise(精简1000-2000字/章) 或 detailed(详细3000-5000字/章)",
"enum": ["concise", "detailed"],
},
},
"required": ["start_chapter", "end_chapter"],
},
"streaming": True,
},
# ═══════════════════════════════════════════════════
# 章节生成与编辑
# ═══════════════════════════════════════════════════
{
"name": "generate_chapter",
"description": "生成指定章节的正文草稿",
"api": {"method": "POST", "path": "/generate/chapter"},
"parameters": {
"type": "object",
"properties": {
"chapter_num": {"type": "integer", "description": "要生成的章节号"},
"word_number": {"type": "integer", "description": "目标字数"},
"user_guidance": {"type": "string", "description": "本章的创作指导"},
"characters_involved": {"type": "string", "description": "本章涉及的角色(逗号分隔)"},
"key_items": {"type": "string", "description": "关键道具"},
"scene_location": {"type": "string", "description": "场景地点"},
"time_constraint": {"type": "string", "description": "时间压力"},
"style_name": {"type": "string", "description": "文风样式名称"},
"narrative_style_name": {"type": "string", "description": "叙事DNA样式名称"},
"xp_type": {"type": "string", "description": "XP类型"},
"inject_world_building": {"type": "boolean", "description": "是否注入世界观"},
"scene_by_scene": {"type": "boolean", "description": "是否按场景分段生成(需有细纲)"},
},
"required": ["chapter_num"],
},
"streaming": True,
},
{
"name": "get_chapter",
"description": "读取已保存的章节内容",
"api": {"method": "GET", "path": "/generate/chapter/{num}"},
"parameters": {
"type": "object",
"properties": {
"num": {"type": "integer", "description": "章节号"},
},
"required": ["num"],
},
"streaming": False,
},
{
"name": "save_chapter",
"description": "保存或覆盖章节内容",
"api": {"method": "PUT", "path": "/generate/chapter/{num}"},
"parameters": {
"type": "object",
"properties": {
"num": {"type": "integer", "description": "章节号"},
"content": {"type": "string", "description": "章节正文内容"},
},
"required": ["num", "content"],
},
"streaming": False,
},
{
"name": "list_chapters",
"description": "列出所有已生成的章节及其状态",
"api": {"method": "GET", "path": "/generate/chapters"},
"parameters": {},
"streaming": False,
},
# ═══════════════════════════════════════════════════
# 润色与精炼
# ═══════════════════════════════════════════════════
{
"name": "polish_chapter",
"description": "对章节进行润色(通用润色/修改剧情/增加内容)",
"api": {"method": "POST", "path": "/generate/expand"},
"parameters": {
"type": "object",
"properties": {
"chapter_num": {"type": "integer", "description": "章节号"},
"polish_guidance": {"type": "string", "description": "润色建议(描述要改什么)"},
"polish_mode": {
"type": "string",
"description": "润色模式",
"enum": ["enhance", "modify", "add"],
},
"style_name": {"type": "string", "description": "文风样式名称"},
"include_outline": {"type": "boolean", "description": "是否注入细纲"},
"include_character_state": {"type": "boolean", "description": "是否注入角色状态"},
"include_summary": {"type": "boolean", "description": "是否注入前文摘要"},
"include_world_building": {"type": "boolean", "description": "是否注入世界观"},
},
"required": ["chapter_num"],
},
"streaming": True,
},
{
"name": "finalize_chapter",
"description": "精炼定稿章节(更新前文摘要、角色状态、向量库)",
"api": {"method": "POST", "path": "/generate/finalize"},
"parameters": {
"type": "object",
"properties": {
"chapter_num": {"type": "integer", "description": "章节号"},
"word_number": {"type": "integer", "description": "目标字数"},
},
"required": ["chapter_num"],
},
"streaming": True,
},
# ═══════════════════════════════════════════════════
# 文件读写(通用)
# ═══════════════════════════════════════════════════
{
"name": "read_file",
"description": "读取项目中的任意文件内容(如架构、蓝图、角色状态、细纲等)",
"api": {"method": "GET", "path": "/files/content"},
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "文件路径相对于项目目录core_seed.txt, Novel_directory.txt, character_state.txt, chapters/chapter_1.txt",
},
},
"required": ["path"],
},
"streaming": False,
},
{
"name": "save_file",
"description": "保存内容到项目中的任意文件",
"api": {"method": "PUT", "path": "/files/content"},
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "文件路径(相对于项目目录)"},
"content": {"type": "string", "description": "要保存的内容"},
},
"required": ["path", "content"],
},
"streaming": False,
},
{
"name": "list_files",
"description": "列出项目目录下的所有文件",
"api": {"method": "GET", "path": "/files"},
"parameters": {},
"streaming": False,
},
# ═══════════════════════════════════════════════════
# 架构组件保存
# ═══════════════════════════════════════════════════
{
"name": "save_architecture_component",
"description": "保存架构的某个组件(核心种子/角色动力学/世界观/情节架构/角色状态)",
"api": {"method": "PUT", "path": "/generate/architecture/component/{component_name}"},
"parameters": {
"type": "object",
"properties": {
"component_name": {
"type": "string",
"description": "组件名称",
"enum": ["core_seed", "character_dynamics", "world_building", "plot_architecture", "character_state"],
},
"content": {"type": "string", "description": "组件内容"},
},
"required": ["component_name", "content"],
},
"streaming": False,
},
{
"name": "save_blueprint",
"description": "保存章节蓝图",
"api": {"method": "PUT", "path": "/generate/blueprint"},
"parameters": {
"type": "object",
"properties": {
"content": {"type": "string", "description": "蓝图内容"},
},
"required": ["content"],
},
"streaming": False,
},
{
"name": "save_detailed_outline",
"description": "保存章节细纲",
"api": {"method": "PUT", "path": "/generate/detailed_outline"},
"parameters": {
"type": "object",
"properties": {
"content": {"type": "string", "description": "细纲内容"},
},
"required": ["content"],
},
"streaming": False,
},
# ═══════════════════════════════════════════════════
# 导出
# ═══════════════════════════════════════════════════
{
"name": "export_novel",
"description": "将所有章节合并导出为完整小说文本",
"api": {"method": "GET", "path": "/generate/export"},
"parameters": {},
"streaming": False,
},
]
def get_tools_for_openai() -> list:
"""转换为 OpenAI function calling 格式"""
tools = []
for t in AGENT_TOOLS:
tool = {
"type": "function",
"function": {
"name": t["name"],
"description": t["description"],
},
}
if t.get("parameters") and t["parameters"].get("properties"):
tool["function"]["parameters"] = t["parameters"]
else:
tool["function"]["parameters"] = {"type": "object", "properties": {}}
tools.append(tool)
return tools
def get_tools_for_claude() -> list:
"""转换为 Claude tool use 格式"""
tools = []
for t in AGENT_TOOLS:
tool = {
"name": t["name"],
"description": t["description"],
"input_schema": t.get("parameters", {"type": "object", "properties": {}}),
}
if not tool["input_schema"].get("properties"):
tool["input_schema"] = {"type": "object", "properties": {}}
tools.append(tool)
return tools
def get_tool_by_name(name: str) -> dict | None:
"""根据工具名获取工具定义"""
for t in AGENT_TOOLS:
if t["name"] == name:
return t
return None
# 工具分类索引(方便 Agent system prompt 中按类别介绍)
TOOL_CATEGORIES = {
"项目管理": ["list_projects", "create_project", "activate_project", "update_project"],
"架构生成": ["generate_core_seed", "generate_characters", "generate_world_building", "generate_plot", "generate_char_state", "assemble_architecture"],
"内容修订": ["revise_content"],
"蓝图与细纲": ["generate_blueprint", "generate_detailed_outline"],
"章节生成": ["generate_chapter", "get_chapter", "save_chapter", "list_chapters"],
"润色与精炼": ["polish_chapter", "finalize_chapter"],
"文件读写": ["read_file", "save_file", "list_files"],
"架构保存": ["save_architecture_component", "save_blueprint", "save_detailed_outline"],
"导出": ["export_novel"],
}
# ═══════════════════════════════════════════════════════════════════════════════
# 工具执行器:通过 HTTP 调用 NovelWriter API
# ═══════════════════════════════════════════════════════════════════════════════
import requests
import json
import urllib.parse
import logging
_logger = logging.getLogger(__name__)
class ToolExecutor:
"""
工具执行器:根据工具名和参数,调用 NovelWriter API 并返回结果。
支持普通 JSON 接口和 SSE 流式接口。
"""
def __init__(self, api_base: str = "http://localhost:7860/api", default_params: dict = None):
"""
Args:
api_base: NovelWriter API 基础地址
default_params: 默认参数(如 llm_config_name, filepath 等),工具调用时自动补全
"""
self.api_base = api_base.rstrip("/")
self.default_params = default_params or {}
def update_defaults(self, **kwargs):
"""更新默认参数"""
self.default_params.update(kwargs)
def execute(self, tool_name: str, params: dict = None) -> dict:
"""
执行工具调用。
Args:
tool_name: 工具名(如 "generate_core_seed"
params: 工具参数
Returns:
{"success": True/False, "data": ..., "error": ...}
"""
tool = get_tool_by_name(tool_name)
if not tool:
return {"success": False, "error": f"未知工具: {tool_name}"}
params = params or {}
# 用默认参数补全缺失的字段
merged = {**self.default_params, **params}
api_info = tool["api"]
method = api_info["method"].upper()
path = api_info["path"]
# 处理路径参数(如 /generate/chapter/{num}
for key in list(merged.keys()):
placeholder = "{" + key + "}"
if placeholder in path:
path = path.replace(placeholder, urllib.parse.quote(str(merged.pop(key))))
# 处理组件名等路径参数
for key in ["component_name", "name"]:
placeholder = "{" + key + "}"
if placeholder in path:
if key in merged:
path = path.replace(placeholder, urllib.parse.quote(str(merged.pop(key))))
url = f"{self.api_base}{path}"
# filepath 需要 URL 编码(中文路径)
if "filepath" in merged and method == "GET":
merged["filepath"] = merged["filepath"] # requests 会自动编码 query params
try:
if tool.get("streaming"):
return self._execute_sse(method, url, merged)
else:
return self._execute_json(method, url, merged)
except Exception as e:
_logger.error(f"工具调用失败 [{tool_name}]: {e}")
return {"success": False, "error": str(e)}
def _execute_json(self, method: str, url: str, params: dict) -> dict:
"""执行普通 JSON 请求"""
if method == "GET":
resp = requests.get(url, params=params, timeout=30)
elif method == "POST":
resp = requests.post(url, json=params, timeout=30)
elif method == "PUT":
resp = requests.put(url, json=params, timeout=30)
elif method == "DELETE":
resp = requests.delete(url, params=params, timeout=30)
else:
return {"success": False, "error": f"不支持的方法: {method}"}
if resp.status_code >= 400:
return {"success": False, "error": f"HTTP {resp.status_code}: {resp.text[:500]}"}
try:
data = resp.json()
except Exception:
data = resp.text
return {"success": True, "data": data}
def _execute_sse(self, method: str, url: str, params: dict) -> dict:
"""执行 SSE 流式请求,等待完成后返回最终结果"""
resp = requests.post(url, json=params, stream=True, timeout=600)
if resp.status_code >= 400:
return {"success": False, "error": f"HTTP {resp.status_code}: {resp.text[:500]}"}
result = ""
last_progress = ""
for line in resp.iter_lines(decode_unicode=True):
if not line or not line.startswith("data: "):
continue
try:
event = json.loads(line[6:])
etype = event.get("type", "")
if etype == "progress":
last_progress = event.get("message", "")
elif etype == "result":
result = event.get("content", "")
elif etype == "error":
return {"success": False, "error": event.get("message", "未知错误")}
except json.JSONDecodeError:
continue
if result:
return {"success": True, "data": result}
elif last_progress:
return {"success": True, "data": last_progress}
else:
return {"success": False, "error": "SSE流结束但未收到结果"}
def load_project_defaults(self):
"""从当前活跃项目中加载默认参数"""
result = self.execute("list_projects")
if not result["success"]:
return
data = result["data"]
active = data.get("active_project", "")
if not active:
return
# 获取项目详情
resp = requests.get(f"{self.api_base}/projects/active", timeout=10)
if resp.status_code != 200:
return
project = resp.json().get("project", {})
self.default_params.update({
"llm_config_name": project.get("llm_config_name", ""),
"emb_config_name": project.get("emb_config_name", ""),
"filepath": project.get("filepath", f"./output/{active}"),
"style_name": project.get("ch_style", ""),
"narrative_style_name": project.get("ch_narrative_style", ""),
"xp_type": project.get("xp_type", ""),
"word_number": project.get("word_number", 3000),
"num_chapters": project.get("num_chapters", 10),
"topic": project.get("topic", ""),
"genre": project.get("genre", ""),
})
_logger.info(f"已加载项目默认参数: {active}")
return project