add rag-retrieve-no-citation
This commit is contained in:
parent
39e8db1ebc
commit
7a30e5297e
12
skills/developing/novare-context/.claude-plugin/plugin.json
Normal file
12
skills/developing/novare-context/.claude-plugin/plugin.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "novare-context",
|
||||
"description": "NOVAREの現在のユーザー詳細情報を自動的に読み込みます",
|
||||
"hooks": {
|
||||
"PrePrompt": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python hooks/pre_prompt.py"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
153
skills/developing/novare-context/README.md
Normal file
153
skills/developing/novare-context/README.md
Normal file
@ -0,0 +1,153 @@
|
||||
# User Context Loader
|
||||
|
||||
用户上下文加载器示例 Skill,演示 Claude Plugins 模式的 hooks 机制。
|
||||
|
||||
## 功能说明
|
||||
|
||||
本 Skill 演示了三种 Hook 类型:
|
||||
|
||||
### PrePrompt Hook
|
||||
在 system_prompt 加载时执行,动态注入用户上下文信息。
|
||||
- 文件: `hooks/pre_prompt.py`
|
||||
- 用途: 查询用户信息、偏好设置、历史记录等,注入到 prompt 中
|
||||
|
||||
### PostAgent Hook
|
||||
在 agent 执行完成后执行,用于后处理。
|
||||
- 文件: `hooks/post_agent.py`
|
||||
- 用途: 记录分析数据、触发异步任务、发送通知等
|
||||
|
||||
### PreSave Hook
|
||||
在消息保存前执行,用于内容处理。
|
||||
- 文件: `hooks/pre_save.py`
|
||||
- 用途: 内容过滤、敏感信息脱敏、格式转换等
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
user-context-loader/
|
||||
├── README.md # Skill 说明文档
|
||||
├── .claude-plugin/
|
||||
│ └── plugin.json # Hook 和 MCP 配置文件
|
||||
└── hooks/
|
||||
├── pre_prompt.py # PrePrompt hook 脚本
|
||||
├── post_agent.py # PostAgent hook 脚本
|
||||
└── pre_save.py # PreSave hook 脚本
|
||||
```
|
||||
|
||||
## plugin.json 格式
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "user-context-loader",
|
||||
"description": "用户上下文加载器示例 Skill",
|
||||
"hooks": {
|
||||
"PrePrompt": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python hooks/pre_prompt.py"
|
||||
}
|
||||
],
|
||||
"PostAgent": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python hooks/post_agent.py"
|
||||
}
|
||||
],
|
||||
"PreSave": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python hooks/pre_save.py"
|
||||
}
|
||||
]
|
||||
},
|
||||
"mcpServers": {
|
||||
"server-name": {
|
||||
"command": "node",
|
||||
"args": ["path/to/server.js"],
|
||||
"env": {
|
||||
"API_KEY": "${API_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Hook 脚本格式
|
||||
|
||||
Hook 脚本通过子进程执行,通过环境变量接收参数,通过 stdout 返回结果。
|
||||
|
||||
### 可用环境变量
|
||||
|
||||
| 环境变量 | 说明 | 适用于 |
|
||||
|---------|------|--------|
|
||||
| `BOT_ID` | Bot ID | 所有 hook |
|
||||
| `USER_IDENTIFIER` | 用户标识 | 所有 hook |
|
||||
| `SESSION_ID` | 会话 ID | 所有 hook |
|
||||
| `LANGUAGE` | 语言代码 | 所有 hook |
|
||||
| `HOOK_TYPE` | Hook 类型 | 所有 hook |
|
||||
| `CONTENT` | 消息内容 | PreSave |
|
||||
| `ROLE` | 消息角色 | PreSave |
|
||||
| `RESPONSE` | Agent 响应 | PostAgent |
|
||||
| `METADATA` | 元数据 JSON | PostAgent |
|
||||
|
||||
### PrePrompt 示例
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
|
||||
def main():
|
||||
user_identifier = os.environ.get('USER_IDENTIFIER', '')
|
||||
bot_id = os.environ.get('BOT_ID', '')
|
||||
|
||||
# 输出要注入到 prompt 中的内容
|
||||
print(f"## User Context\n\n用户: {user_identifier}")
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
```
|
||||
|
||||
### PreSave 示例
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
|
||||
def main():
|
||||
content = os.environ.get('CONTENT', '')
|
||||
|
||||
# 处理内容并输出
|
||||
print(content) # 输出处理后的内容
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
```
|
||||
|
||||
### PostAgent 示例
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
|
||||
def main():
|
||||
response = os.environ.get('RESPONSE', '')
|
||||
session_id = os.environ.get('SESSION_ID', '')
|
||||
|
||||
# 记录日志(输出到 stderr)
|
||||
print(f"Session {session_id}: Response length {len(response)}", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
```
|
||||
|
||||
## 使用场景
|
||||
|
||||
1. **PrePrompt**: 用户登录时自动加载其偏好设置、历史订单等
|
||||
2. **PostAgent**: 记录对话分析数据,触发后续业务流程
|
||||
3. **PreSave**: 敏感信息脱敏后再存储,如手机号、邮箱等
|
||||
166
skills/developing/novare-context/hooks/pre_prompt.py
Normal file
166
skills/developing/novare-context/hooks/pre_prompt.py
Normal file
@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PrePrompt Hook - 用户上下文加载器
|
||||
|
||||
在 system_prompt 加载时执行,通过 MCP 服务查询用户相关信息并注入到 prompt 中。
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import anyio
|
||||
from mcp.client.session import ClientSession
|
||||
from mcp.client.streamable_http import streamablehttp_client
|
||||
|
||||
# MCP 服务配置
|
||||
MCP_BASE_URL = "http://prd-mcp.gbase.ai/mcp/iot/sse"
|
||||
MCP_TIMEOUT = 30
|
||||
|
||||
|
||||
async def get_employee_location(user_identifier: str, bot_id: str) -> dict | None:
|
||||
"""
|
||||
通过 MCP 服务查询员工位置信息
|
||||
|
||||
Args:
|
||||
user_identifier: 用户标识(员工姓名或邮箱)
|
||||
bot_id: Bot ID(当前未使用,保留以备将来扩展)
|
||||
|
||||
Returns:
|
||||
员工位置信息字典,如果查询失败返回 None
|
||||
"""
|
||||
try:
|
||||
async with streamablehttp_client(
|
||||
url=MCP_BASE_URL,
|
||||
timeout=MCP_TIMEOUT,
|
||||
terminate_on_close=False,
|
||||
) as (read_stream, write_stream, _):
|
||||
async with ClientSession(read_stream, write_stream) as session:
|
||||
# 初始化 MCP 会话
|
||||
await session.initialize()
|
||||
|
||||
# 调用 find_employee_location 工具
|
||||
result = await session.call_tool(
|
||||
name="find_employee_location",
|
||||
arguments={"name": user_identifier}
|
||||
)
|
||||
|
||||
# 解析返回结果
|
||||
if result.content:
|
||||
for item in result.content:
|
||||
if hasattr(item, 'text') and item.text:
|
||||
try:
|
||||
return json.loads(item.text)
|
||||
except json.JSONDecodeError:
|
||||
# 如果不是 JSON,直接返回文本
|
||||
return {"data": item.text}
|
||||
|
||||
return None
|
||||
|
||||
except Exception:
|
||||
# 发生错误时返回 None,不影响主流程
|
||||
return None
|
||||
|
||||
|
||||
def format_location_context(location_data: dict | None, user_identifier: str, bot_id: str) -> str:
|
||||
"""
|
||||
格式化位置信息为 Markdown 上下文(日语)
|
||||
|
||||
Args:
|
||||
location_data: 从 MCP 查询返回的位置数据
|
||||
user_identifier: 用户标识(不使用)
|
||||
bot_id: Bot ID(不使用)
|
||||
|
||||
Returns:
|
||||
格式化后的 Markdown 字符串,出错或无数据时返回空字符串
|
||||
"""
|
||||
# 出错或无数据时返回空字符串
|
||||
if not location_data:
|
||||
return ""
|
||||
|
||||
matched_count = location_data.get('matched_count', 0)
|
||||
results = location_data.get('results', [])
|
||||
|
||||
# 没有匹配数据时返回空字符串
|
||||
if matched_count == 0:
|
||||
return ""
|
||||
|
||||
lines = []
|
||||
|
||||
# 添加说明:这是当前用户(USER_IDENTIFIER)的信息
|
||||
lines.append(f"**Current User ({user_identifier}) Information**:")
|
||||
lines.append("")
|
||||
|
||||
for idx, employee in enumerate(results, 1):
|
||||
name = employee.get('name', 'Unknown')
|
||||
sensor_id = employee.get('sensor_id', '')
|
||||
confidence = employee.get('confidence', 0)
|
||||
|
||||
lines.append(f"- Name: {name}")
|
||||
lines.append(f"- Sensor ID: {sensor_id}")
|
||||
|
||||
location_status = employee.get('location_status', '')
|
||||
|
||||
if location_status == 'success':
|
||||
coordinates = employee.get('coordinates', {})
|
||||
location = employee.get('location', {})
|
||||
|
||||
lines.append(f"- Location Status: Success")
|
||||
lines.append(f"- Floor: {coordinates.get('floor', 'N/A')}")
|
||||
lines.append(f"- Coordinates: ({coordinates.get('x', 0):.2f}, {coordinates.get('y', 0):.2f}, {coordinates.get('z', 0):.2f})")
|
||||
|
||||
if location:
|
||||
building = location.get('building', '')
|
||||
area = location.get('area', '')
|
||||
room = location.get('room', '')
|
||||
lines.append(f"- Detailed Location: {building} / {area} / {room}")
|
||||
|
||||
measurement_time = employee.get('measurement_time')
|
||||
if measurement_time:
|
||||
lines.append(f"- Measurement Time: {measurement_time}")
|
||||
|
||||
elif location_status == 'not_in_range':
|
||||
lines.append(f"- Location Status: Out of Range")
|
||||
error_message = employee.get('error_message', '')
|
||||
if error_message:
|
||||
lines.append(f"- Note: {error_message}")
|
||||
|
||||
else: # error
|
||||
lines.append(f"- Location Status: Failed")
|
||||
error_message = employee.get('error_message', 'Unknown Error')
|
||||
lines.append(f"- Error: {error_message}")
|
||||
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
async def async_main():
|
||||
"""异步主函数"""
|
||||
user_identifier = os.environ.get('USER_IDENTIFIER', '')
|
||||
bot_id = os.environ.get('BOT_ID', '')
|
||||
|
||||
if not user_identifier:
|
||||
return 0
|
||||
|
||||
# 查询员工位置信息
|
||||
location_data = await get_employee_location(user_identifier, bot_id)
|
||||
|
||||
# 格式化并输出上下文(出错或无数据时返回空字符串)
|
||||
context_info = format_location_context(location_data, user_identifier, bot_id)
|
||||
if context_info:
|
||||
print(context_info)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
"""从环境变量读取参数并通过 MCP 服务查询用户上下文"""
|
||||
try:
|
||||
return anyio.run(async_main)
|
||||
except Exception:
|
||||
# 出错时返回空字符串(不输出任何内容)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "rag-retrieve-no-citation",
|
||||
"description": "Only provides rag_retrieve without citation. table_rag_retrieve and local file retrieval are disabled.",
|
||||
"hooks": {
|
||||
"PrePrompt": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python hooks/pre_prompt.py"
|
||||
}
|
||||
]
|
||||
},
|
||||
"mcpServers": {
|
||||
"rag_retrieve": {
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": [
|
||||
"./rag_retrieve_server.py",
|
||||
"{bot_id}"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
34
skills/developing/rag-retrieve-no-citation/README.md
Normal file
34
skills/developing/rag-retrieve-no-citation/README.md
Normal file
@ -0,0 +1,34 @@
|
||||
# rag-retrieve
|
||||
|
||||
只保留 `rag_retrieve` 的精简版插件示例。
|
||||
|
||||
## 功能说明
|
||||
|
||||
- 通过 `PrePrompt` Hook 注入检索策略
|
||||
- 暴露 `rag_retrieve` MCP Server
|
||||
- 插件仅支持 `rag_retrieve`
|
||||
- 已禁用 `table_rag_retrieve`
|
||||
- 已禁用本地文件检索
|
||||
|
||||
## 目录结构
|
||||
|
||||
```text
|
||||
rag-retrieve-only/
|
||||
├── README.md
|
||||
├── .claude-plugin/
|
||||
│ └── plugin.json
|
||||
├── hooks/
|
||||
│ ├── pre_prompt.py
|
||||
│ └── retrieval-policy.md
|
||||
├── rag_retrieve_server.py
|
||||
└── rag_retrieve_tools.json
|
||||
```
|
||||
|
||||
## 当前检索策略
|
||||
|
||||
默认顺序:skill-enabled knowledge retrieval tools > `rag_retrieve`
|
||||
|
||||
- 优先使用可用的技能内知识检索工具
|
||||
- 不足时使用 `rag_retrieve`
|
||||
- 不并行执行多个检索源
|
||||
- 插件仅支持 `rag_retrieve`
|
||||
@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PreMemoryPrompt Hook - 用户上下文加载器示例
|
||||
|
||||
在记忆提取提示词(FACT_RETRIEVAL_PROMPT)加载时执行,
|
||||
根据环境变量决定是否启用禁止使用模型自身知识的 retrieval policy。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
enable_self_knowledge = (
|
||||
os.getenv("ENABLE_SELF_KNOWLEDGE", "false").lower() == "true"
|
||||
)
|
||||
policy_name = (
|
||||
"retrieval-policy.md"
|
||||
if enable_self_knowledge
|
||||
else "retrieval-policy-forbidden-self-knowledge.md"
|
||||
)
|
||||
prompt_file = Path(__file__).parent / policy_name
|
||||
print(prompt_file.read_text(encoding="utf-8"))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@ -0,0 +1,111 @@
|
||||
# Retrieval Policy (Forbidden Self-Knowledge)
|
||||
|
||||
## 0. Task Classification
|
||||
|
||||
Classify the request before acting:
|
||||
- **Knowledge retrieval** (facts, summaries, comparisons, prices, lists, timelines, extraction, etc.): follow this policy strictly.
|
||||
- **Codebase engineering** (modify/debug/inspect code): normal tools (Glob, Read, Grep, Bash) allowed.
|
||||
- **Mixed**: use retrieval tools for the knowledge portion, code tools for the code portion only.
|
||||
- **Uncertain**: default to knowledge retrieval.
|
||||
|
||||
## 1. Critical Enforcement
|
||||
|
||||
For knowledge retrieval tasks, **this policy overrides generic codebase exploration behavior**.
|
||||
|
||||
- **Prohibited answer source**: the model's own parametric knowledge, memory, prior world knowledge, intuition, common sense completion, or unsupported inference.
|
||||
- **Prohibited tools**: `Glob`, `Read`, `LS`, Bash (`ls`, `find`, `cat`, `head`, `tail`, `grep`, etc.) — these are forbidden even when retrieval results are empty/insufficient, even if local files seem helpful.
|
||||
- **Allowed tools only**: skill-enabled retrieval tools, `rag_retrieve`. No other source for factual answering.
|
||||
- Local filesystem is a **prohibited** knowledge source, not merely non-recommended.
|
||||
- Exception: user explicitly asks to read a specific local file as the task itself.
|
||||
- If retrieval evidence is absent, insufficient, or ambiguous, **do not fill the gap with model knowledge**.
|
||||
|
||||
## 2. Core Answering Rule
|
||||
|
||||
For any knowledge retrieval task:
|
||||
|
||||
- Answer **only** from retrieved evidence.
|
||||
- Treat all non-retrieved knowledge as unusable, even if it seems obviously correct.
|
||||
- Do NOT answer from memory first.
|
||||
- Do NOT "helpfully complete" missing facts.
|
||||
- Do NOT convert weak hints into confident statements.
|
||||
- If evidence does not support a claim, omit the claim.
|
||||
|
||||
## 3. Retrieval Order and Tool Selection
|
||||
|
||||
Execute **sequentially, one at a time**. Do NOT run in parallel. Do NOT probe filesystem first.
|
||||
|
||||
1. **Skill-enabled retrieval tools** (use first when available)
|
||||
2. **`rag_retrieve`**
|
||||
|
||||
- After each step, evaluate sufficiency before proceeding.
|
||||
- Retrieval must happen **before** any factual answer generation.
|
||||
|
||||
## 4. Query Preparation
|
||||
|
||||
- Do NOT pass raw user question unless it already works well for retrieval.
|
||||
- Rewrite for recall: extract entity, time scope, attributes, intent. Add synonyms, aliases, abbreviations, historical names, category terms.
|
||||
- Expand list/extraction/overview/timeline queries more aggressively. Preserve meaning.
|
||||
|
||||
## 5. Retrieval Breadth (`top_k`)
|
||||
|
||||
- Apply `top_k` only to `rag_retrieve`. Use smallest sufficient value, expand if insufficient.
|
||||
- `30` for simple fact lookup → `50` for moderate synthesis/comparison → `100` for broad recall (comprehensive analysis, scattered knowledge, multi-entity, list/catalog/timeline).
|
||||
- Expansion order: `30 → 50 → 100`. If unsure, use `100`.
|
||||
|
||||
## 6. Result Evaluation
|
||||
|
||||
Treat as insufficient if: empty, `Error:`, off-topic, missing core entity/scope, no usable evidence, partial coverage, truncated results, or claims required by the answer are not explicitly supported.
|
||||
|
||||
## 7. Fallback and Sequential Retry
|
||||
|
||||
On insufficient results, follow this sequence:
|
||||
|
||||
1. Rewrite query, retry same tool (once)
|
||||
2. Switch to next retrieval source in default order
|
||||
3. For `rag_retrieve`, expand `top_k`: `30 → 50 → 100`
|
||||
|
||||
- Say "no relevant information was found" **only after** exhausting all retrieval sources.
|
||||
- Do NOT switch to local filesystem inspection at any point.
|
||||
- Do NOT switch to model self-knowledge at any point.
|
||||
|
||||
## 8. Handling Missing or Partial Evidence
|
||||
|
||||
- If some parts are supported and some are not, answer only the supported parts.
|
||||
- Clearly mark unsupported parts as unavailable rather than guessing.
|
||||
- Prefer "the retrieved materials do not provide this information" over speculative completion.
|
||||
- When user asks for a definitive answer but evidence is incomplete, state the limitation directly.
|
||||
|
||||
## 9. Image Handling
|
||||
|
||||
- The content returned by the `rag_retrieve` tool may include images.
|
||||
- Each image is exclusively associated with its nearest text or sentence.
|
||||
- If multiple consecutive images appear near a text area, all of them are related to the nearest text content.
|
||||
- Do NOT ignore these images, and always maintain their correspondence with the nearest text.
|
||||
- Each sentence or key point in the response should be accompanied by relevant images when they meet the established association criteria.
|
||||
- Avoid placing all images at the end of the response.
|
||||
|
||||
|
||||
## 10. Self-Knowledge Prohibition
|
||||
|
||||
This section applies whenever self-knowledge is disabled or forbidden for the current task.
|
||||
|
||||
- Retrieval remains the only usable source for factual answering.
|
||||
- If retrieval is sufficient, answer from retrieval only.
|
||||
- If retrieval is partially sufficient, answer only the supported parts.
|
||||
- The model must not supplement missing parts with general knowledge, conceptual explanation, common background, intuition, or likely completion.
|
||||
- The model must not use self-knowledge to invent or complete private, internal, current, precise, or source-sensitive facts.
|
||||
- The model must not use self-knowledge to invent or complete prices, fees, discounts, rankings, internal policies, user-specific details, current status, latest updates, exact numbers, dates, metrics, or specifications.
|
||||
- Unsupported parts must be stated as unavailable rather than guessed.
|
||||
- If a paragraph would mix retrieved facts and unsupported completion, remove the unsupported completion.
|
||||
- If evidence is incomplete, state the limitation explicitly.
|
||||
|
||||
## 11. Pre-Reply Self-Check
|
||||
|
||||
Before replying to a knowledge retrieval task, verify:
|
||||
- Used only whitelisted retrieval tools — no local filesystem inspection?
|
||||
- Did retrieval happen before any factual answer drafting?
|
||||
- Did every factual claim come from retrieved evidence rather than model knowledge?
|
||||
- Exhausted retrieval flow before concluding "not found"?
|
||||
- If any unsupported part remained, was it removed or explicitly marked unavailable?
|
||||
|
||||
If any answer is "no", correct the process first.
|
||||
@ -0,0 +1,87 @@
|
||||
# Retrieval Policy
|
||||
|
||||
## 0. Task Classification
|
||||
|
||||
Classify the request before acting:
|
||||
- **Knowledge retrieval** (facts, summaries, comparisons, prices, lists, timelines, extraction, etc.): follow this policy strictly.
|
||||
- **Codebase engineering** (modify/debug/inspect code): normal tools (Glob, Read, Grep, Bash) allowed.
|
||||
- **Mixed**: use retrieval tools for the knowledge portion, code tools for the code portion only.
|
||||
- **Uncertain**: default to knowledge retrieval.
|
||||
|
||||
## 1. Critical Enforcement
|
||||
|
||||
For knowledge retrieval tasks, **this policy overrides generic codebase exploration behavior**.
|
||||
|
||||
- **Prohibited tools**: `Glob`, `Read`, `LS`, Bash (`ls`, `find`, `cat`, `head`, `tail`, `grep`, etc.) — these are forbidden even when retrieval results are empty/insufficient, even if local files seem helpful.
|
||||
- **Allowed tools only**: skill-enabled retrieval tools, `rag_retrieve`. No other source for factual answering.
|
||||
- Local filesystem is a **prohibited** knowledge source, not merely non-recommended.
|
||||
- Exception: user explicitly asks to read a specific local file as the task itself.
|
||||
|
||||
## 2. Retrieval Order and Tool Selection
|
||||
|
||||
Execute **sequentially, one at a time**. Do NOT run in parallel. Do NOT probe filesystem first.
|
||||
|
||||
1. **Skill-enabled retrieval tools** (use first when available)
|
||||
2. **`rag_retrieve`**
|
||||
|
||||
- Do NOT answer from model knowledge first.
|
||||
- After each step, evaluate sufficiency before proceeding.
|
||||
|
||||
## 3. Query Preparation
|
||||
|
||||
- Do NOT pass raw user question unless it already works well for retrieval.
|
||||
- Rewrite for recall: extract entity, time scope, attributes, intent. Add synonyms, aliases, abbreviations, historical names, category terms.
|
||||
- Expand list/extraction/overview/timeline queries more aggressively. Preserve meaning.
|
||||
|
||||
## 4. Retrieval Breadth (`top_k`)
|
||||
|
||||
- Apply `top_k` only to `rag_retrieve`. Use smallest sufficient value, expand if insufficient.
|
||||
- `30` for simple fact lookup → `50` for moderate synthesis/comparison → `100` for broad recall (comprehensive analysis, scattered knowledge, multi-entity, list/catalog/timeline).
|
||||
- Expansion order: `30 → 50 → 100`. If unsure, use `100`.
|
||||
|
||||
## 5. Result Evaluation
|
||||
|
||||
Treat as insufficient if: empty, `Error:`, off-topic, missing core entity/scope, no usable evidence, partial coverage, or truncated results.
|
||||
|
||||
## 6. Fallback and Sequential Retry
|
||||
|
||||
On insufficient results, follow this sequence:
|
||||
|
||||
1. Rewrite query, retry same tool (once)
|
||||
2. Switch to next retrieval source in default order
|
||||
3. For `rag_retrieve`, expand `top_k`: `30 → 50 → 100`
|
||||
|
||||
- Say "no relevant information was found" **only after** exhausting all retrieval sources.
|
||||
- Do NOT switch to local filesystem inspection at any point.
|
||||
|
||||
## 7. Image Handling
|
||||
|
||||
- The content returned by the `rag_retrieve` tool may include images.
|
||||
- Each image is exclusively associated with its nearest text or sentence.
|
||||
- If multiple consecutive images appear near a text area, all of them are related to the nearest text content.
|
||||
- Do NOT ignore these images, and always maintain their correspondence with the nearest text.
|
||||
- Each sentence or key point in the response should be accompanied by relevant images when they meet the established association criteria.
|
||||
- Avoid placing all images at the end of the response.
|
||||
|
||||
## 8. Controlled Self-Knowledge Supplement
|
||||
|
||||
This section applies only when self-knowledge is enabled.
|
||||
|
||||
- Retrieval remains the primary source.
|
||||
- If retrieval is sufficient, answer from retrieval only.
|
||||
- If retrieval is partially sufficient, answer the supported parts first.
|
||||
- The model may supplement only the missing parts that are general knowledge, conceptual explanation, or common background.
|
||||
- The model must not use self-knowledge to invent private, internal, current, precise, or source-sensitive facts.
|
||||
- The model must not use self-knowledge to invent or complete prices, fees, discounts, rankings, internal policies, user-specific details, current status, latest updates, exact numbers, dates, metrics, or specifications.
|
||||
- Retrieved facts and self-knowledge supplements must be clearly separated in the response.
|
||||
- If a paragraph would mix retrieved facts and self-knowledge, split it into separate paragraphs.
|
||||
- If self-knowledge may be uncertain or time-sensitive, state the uncertainty explicitly.
|
||||
|
||||
## 9. Pre-Reply Self-Check
|
||||
|
||||
Before replying to a knowledge retrieval task, verify:
|
||||
- Used only whitelisted retrieval tools — no local filesystem inspection?
|
||||
- Exhausted retrieval flow before concluding "not found"?
|
||||
- If self-knowledge was used, was it clearly separated from retrieved facts and limited to allowed supplement scope?
|
||||
|
||||
If any answer is "no", correct the process first.
|
||||
251
skills/developing/rag-retrieve-no-citation/mcp_common.py
Normal file
251
skills/developing/rag-retrieve-no-citation/mcp_common.py
Normal file
@ -0,0 +1,251 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MCP服务器通用工具函数
|
||||
提供路径处理、文件验证、请求处理等公共功能
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
import re
|
||||
|
||||
def get_allowed_directory():
|
||||
"""获取允许访问的目录"""
|
||||
# 优先使用命令行参数传入的dataset_dir
|
||||
if len(sys.argv) > 1:
|
||||
dataset_dir = sys.argv[1]
|
||||
return os.path.abspath(dataset_dir)
|
||||
|
||||
# 从环境变量读取项目数据目录
|
||||
project_dir = os.getenv("PROJECT_DATA_DIR", "./projects/data")
|
||||
return os.path.abspath(project_dir)
|
||||
|
||||
|
||||
def resolve_file_path(file_path: str, default_subfolder: str = "default") -> str:
|
||||
"""
|
||||
解析文件路径,支持 folder/document.txt 和 document.txt 两种格式
|
||||
|
||||
Args:
|
||||
file_path: 输入的文件路径
|
||||
default_subfolder: 当只传入文件名时使用的默认子文件夹名称
|
||||
|
||||
Returns:
|
||||
解析后的完整文件路径
|
||||
"""
|
||||
# 如果路径包含文件夹分隔符,直接使用
|
||||
if '/' in file_path or '\\' in file_path:
|
||||
clean_path = file_path.replace('\\', '/')
|
||||
|
||||
# 移除 projects/ 前缀(如果存在)
|
||||
if clean_path.startswith('projects/'):
|
||||
clean_path = clean_path[9:] # 移除 'projects/' 前缀
|
||||
elif clean_path.startswith('./projects/'):
|
||||
clean_path = clean_path[11:] # 移除 './projects/' 前缀
|
||||
else:
|
||||
# 如果只有文件名,添加默认子文件夹
|
||||
clean_path = f"{default_subfolder}/{file_path}"
|
||||
|
||||
# 获取允许的目录
|
||||
project_data_dir = get_allowed_directory()
|
||||
|
||||
# 尝试在项目目录中查找文件
|
||||
full_path = os.path.join(project_data_dir, clean_path.lstrip('./'))
|
||||
if os.path.exists(full_path):
|
||||
return full_path
|
||||
|
||||
# 如果直接路径不存在,尝试递归查找
|
||||
found = find_file_in_project(clean_path, project_data_dir)
|
||||
if found:
|
||||
return found
|
||||
|
||||
# 如果是纯文件名且在default子文件夹中不存在,尝试在根目录查找
|
||||
if '/' not in file_path and '\\' not in file_path:
|
||||
root_path = os.path.join(project_data_dir, file_path)
|
||||
if os.path.exists(root_path):
|
||||
return root_path
|
||||
|
||||
raise FileNotFoundError(f"File not found: {file_path} (searched in {project_data_dir})")
|
||||
|
||||
|
||||
def find_file_in_project(filename: str, project_dir: str) -> Optional[str]:
|
||||
"""在项目目录中递归查找文件"""
|
||||
# 如果filename包含路径,只搜索指定的路径
|
||||
if '/' in filename:
|
||||
parts = filename.split('/')
|
||||
target_file = parts[-1]
|
||||
search_dir = os.path.join(project_dir, *parts[:-1])
|
||||
|
||||
if os.path.exists(search_dir):
|
||||
target_path = os.path.join(search_dir, target_file)
|
||||
if os.path.exists(target_path):
|
||||
return target_path
|
||||
else:
|
||||
# 纯文件名,递归搜索整个项目目录
|
||||
for root, dirs, files in os.walk(project_dir):
|
||||
if filename in files:
|
||||
return os.path.join(root, filename)
|
||||
return None
|
||||
|
||||
|
||||
def load_tools_from_json(tools_file_name: str) -> List[Dict[str, Any]]:
|
||||
"""从 JSON 文件加载工具定义"""
|
||||
try:
|
||||
tools_file = os.path.join(os.path.dirname(__file__), tools_file_name)
|
||||
if os.path.exists(tools_file):
|
||||
with open(tools_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
else:
|
||||
# 如果 JSON 文件不存在,使用默认定义
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"Warning: Unable to load tool definition JSON file: {str(e)}")
|
||||
return []
|
||||
|
||||
|
||||
def create_error_response(request_id: Any, code: int, message: str) -> Dict[str, Any]:
|
||||
"""创建标准化的错误响应"""
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def create_success_response(request_id: Any, result: Any) -> Dict[str, Any]:
|
||||
"""创建标准化的成功响应"""
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": result
|
||||
}
|
||||
|
||||
|
||||
def create_initialize_response(request_id: Any, server_name: str, server_version: str = "1.0.0") -> Dict[str, Any]:
|
||||
"""创建标准化的初始化响应"""
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {
|
||||
"tools": {}
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": server_name,
|
||||
"version": server_version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def create_ping_response(request_id: Any) -> Dict[str, Any]:
|
||||
"""创建标准化的ping响应"""
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"pong": True
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def create_tools_list_response(request_id: Any, tools: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""创建标准化的工具列表响应"""
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"tools": tools
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def is_regex_pattern(pattern: str) -> bool:
|
||||
"""检测字符串是否为正则表达式模式"""
|
||||
# 检查 /pattern/ 格式
|
||||
if pattern.startswith('/') and pattern.endswith('/') and len(pattern) > 2:
|
||||
return True
|
||||
|
||||
# 检查 r"pattern" 或 r'pattern' 格式
|
||||
if pattern.startswith(('r"', "r'")) and pattern.endswith(('"', "'")) and len(pattern) > 3:
|
||||
return True
|
||||
|
||||
# 检查是否包含正则特殊字符
|
||||
regex_chars = {'*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '^', '$', '\\', '.'}
|
||||
return any(char in pattern for char in regex_chars)
|
||||
|
||||
|
||||
def compile_pattern(pattern: str) -> Union[re.Pattern, str, None]:
|
||||
"""编译正则表达式模式,如果不是正则则返回原字符串"""
|
||||
if not is_regex_pattern(pattern):
|
||||
return pattern
|
||||
|
||||
try:
|
||||
# 处理 /pattern/ 格式
|
||||
if pattern.startswith('/') and pattern.endswith('/'):
|
||||
regex_body = pattern[1:-1]
|
||||
return re.compile(regex_body)
|
||||
|
||||
# 处理 r"pattern" 或 r'pattern' 格式
|
||||
if pattern.startswith(('r"', "r'")) and pattern.endswith(('"', "'")):
|
||||
regex_body = pattern[2:-1]
|
||||
return re.compile(regex_body)
|
||||
|
||||
# 直接编译包含正则字符的字符串
|
||||
return re.compile(pattern)
|
||||
except re.error as e:
|
||||
# 如果编译失败,返回None表示无效的正则
|
||||
print(f"Warning: Regular expression '{pattern}' compilation failed: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def handle_mcp_streaming(request_handler):
|
||||
"""处理MCP请求的标准主循环"""
|
||||
try:
|
||||
while True:
|
||||
# Read from stdin
|
||||
line = await asyncio.get_event_loop().run_in_executor(None, sys.stdin.readline)
|
||||
if not line:
|
||||
break
|
||||
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
try:
|
||||
request = json.loads(line)
|
||||
response = await request_handler(request)
|
||||
|
||||
# Write to stdout
|
||||
sys.stdout.write(json.dumps(response, ensure_ascii=False) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
except json.JSONDecodeError:
|
||||
error_response = {
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32700,
|
||||
"message": "Parse error"
|
||||
}
|
||||
}
|
||||
sys.stdout.write(json.dumps(error_response, ensure_ascii=False) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
except Exception as e:
|
||||
error_response = {
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32603,
|
||||
"message": f"Internal error: {str(e)}"
|
||||
}
|
||||
}
|
||||
sys.stdout.write(json.dumps(error_response, ensure_ascii=False) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
RAG检索MCP服务器
|
||||
调用本地RAG API进行文档检索
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from typing import Any, Dict
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("Error: requests module is required. Please install it with: pip install requests")
|
||||
sys.exit(1)
|
||||
|
||||
from mcp_common import (
|
||||
create_error_response,
|
||||
create_success_response,
|
||||
create_initialize_response,
|
||||
create_ping_response,
|
||||
create_tools_list_response,
|
||||
load_tools_from_json,
|
||||
handle_mcp_streaming
|
||||
)
|
||||
BACKEND_HOST = os.getenv("BACKEND_HOST", "https://api-dev.gptbase.ai")
|
||||
MASTERKEY = os.getenv("MASTERKEY", "master")
|
||||
|
||||
def rag_retrieve(query: str, top_k: int = 100) -> Dict[str, Any]:
|
||||
"""调用RAG检索API"""
|
||||
try:
|
||||
bot_id = ""
|
||||
if len(sys.argv) > 1:
|
||||
bot_id = sys.argv[1]
|
||||
|
||||
url = f"{BACKEND_HOST}/v1/rag_retrieve/{bot_id}"
|
||||
if not url:
|
||||
return {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Error: RAG API URL not provided. Please provide URL as command line argument."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
masterkey = MASTERKEY
|
||||
token_input = f"{masterkey}:{bot_id}"
|
||||
auth_token = hashlib.md5(token_input.encode()).hexdigest()
|
||||
|
||||
headers = {
|
||||
"content-type": "application/json",
|
||||
"authorization": f"Bearer {auth_token}"
|
||||
}
|
||||
data = {
|
||||
"query": query,
|
||||
"top_k": top_k
|
||||
}
|
||||
|
||||
response = requests.post(url, json=data, headers=headers, timeout=30)
|
||||
|
||||
if response.status_code != 200:
|
||||
return {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": f"Error: RAG API returned status code {response.status_code}. Response: {response.text}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
try:
|
||||
response_data = response.json()
|
||||
except json.JSONDecodeError as e:
|
||||
return {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": f"Error: Failed to parse API response as JSON. Error: {str(e)}, Raw response: {response.text}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if "markdown" in response_data:
|
||||
markdown_content = response_data["markdown"]
|
||||
return {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": markdown_content
|
||||
}
|
||||
]
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": f"Error: 'markdown' field not found in API response. Response: {json.dumps(response_data, indent=2, ensure_ascii=False)}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
return {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": f"Error: Failed to connect to RAG API. {str(e)}"
|
||||
}
|
||||
]
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": f"Error: {str(e)}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
async def handle_request(request: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Handle MCP request"""
|
||||
try:
|
||||
method = request.get("method")
|
||||
params = request.get("params", {})
|
||||
request_id = request.get("id")
|
||||
|
||||
if method == "initialize":
|
||||
return create_initialize_response(request_id, "rag-retrieve")
|
||||
|
||||
elif method == "ping":
|
||||
return create_ping_response(request_id)
|
||||
|
||||
elif method == "tools/list":
|
||||
tools = load_tools_from_json("rag_retrieve_tools.json")
|
||||
if not tools:
|
||||
tools = [
|
||||
{
|
||||
"name": "rag_retrieve",
|
||||
"description": "调用RAG检索API,根据查询内容检索相关文档。返回包含相关内容的markdown格式结果。",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "检索查询内容"
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
}
|
||||
]
|
||||
return create_tools_list_response(request_id, tools)
|
||||
|
||||
elif method == "tools/call":
|
||||
tool_name = params.get("name")
|
||||
arguments = params.get("arguments", {})
|
||||
|
||||
if tool_name == "rag_retrieve":
|
||||
query = arguments.get("query", "")
|
||||
top_k = arguments.get("top_k", 100)
|
||||
|
||||
if not query:
|
||||
return create_error_response(request_id, -32602, "Missing required parameter: query")
|
||||
|
||||
result = rag_retrieve(query, top_k)
|
||||
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": result
|
||||
}
|
||||
|
||||
else:
|
||||
return create_error_response(request_id, -32601, f"Unknown tool: {tool_name}")
|
||||
|
||||
else:
|
||||
return create_error_response(request_id, -32601, f"Unknown method: {method}")
|
||||
|
||||
except Exception as e:
|
||||
return create_error_response(request.get("id"), -32603, f"Internal error: {str(e)}")
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main entry point."""
|
||||
await handle_mcp_streaming(handle_request)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@ -0,0 +1,21 @@
|
||||
[
|
||||
{
|
||||
"name": "rag_retrieve",
|
||||
"description": "Retrieve relevant documents from the knowledge base. Returns markdown results. Use this tool for concept, definition, workflow, policy, explanation, and general knowledge lookup. Rewrite the query when needed to improve recall.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Retrieval query content. Rewrite the query when needed to improve recall."
|
||||
},
|
||||
"top_k": {
|
||||
"type": "integer",
|
||||
"description": "Number of top results to retrieve. Choose dynamically based on retrieval breadth and coverage needs.",
|
||||
"default": 100
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
}
|
||||
]
|
||||
Loading…
Reference in New Issue
Block a user