WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1 @@
|
||||
.venv/
|
||||
@@ -0,0 +1,85 @@
|
||||
# WeKnora 本地 MCP Demo
|
||||
|
||||
最小外部 MCP 服务,用来测试 WeKnora **作为 MCP 客户端**接入第三方工具。
|
||||
|
||||
提供 6 个演示工具:
|
||||
|
||||
| 工具 | 作用 |
|
||||
| --- | --- |
|
||||
| `echo` | 连通性自检 |
|
||||
| `add` | 两数相加 |
|
||||
| `server_time` | 返回服务器 UTC 时间 |
|
||||
| `lookup_policy` | 查询演示政策(保修、报销、POC 等,与 `website-docs/sample-data/` 一致) |
|
||||
| `list_team_contacts` | 列出演示项目团队成员 |
|
||||
| `send_demo_alert` | 模拟外发通知(适合测工具人工审批) |
|
||||
|
||||
## 1. 启动
|
||||
|
||||
```bash
|
||||
cd examples/mcp-demo
|
||||
chmod +x start.sh
|
||||
./start.sh
|
||||
```
|
||||
|
||||
`start.sh` 会自动创建 `.venv` 并安装依赖。默认监听 `http://127.0.0.1:8010/mcp`,鉴权令牌 `weknora-demo-token`。
|
||||
|
||||
自定义:
|
||||
|
||||
```bash
|
||||
export MCP_SERVER_AUTH_TOKEN=my-secret
|
||||
export MCP_PORT=9000
|
||||
./start.sh
|
||||
```
|
||||
|
||||
## 2. 自检
|
||||
|
||||
另开终端:
|
||||
|
||||
```bash
|
||||
cd examples/mcp-demo
|
||||
source .venv/bin/activate
|
||||
python test_tools.py
|
||||
```
|
||||
|
||||
应列出 6 个工具。
|
||||
|
||||
## 3. 接入 WeKnora
|
||||
|
||||
1. 打开 **设置 → MCP 服务 → 新建**
|
||||
2. 填写:
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| 名称 | `本地 MCP Demo` |
|
||||
| 传输 | **HTTP Streamable** |
|
||||
| URL | `http://127.0.0.1:8010/mcp` |
|
||||
| 认证 | **Bearer** |
|
||||
| 令牌 | `weknora-demo-token`(与 `MCP_SERVER_AUTH_TOKEN` 一致) |
|
||||
|
||||
3. 保存后点 **测试连接**,应发现 6 个工具。
|
||||
4. 在 **智能体** 配置里勾选该 MCP 服务(或选全部工具)。
|
||||
5. (可选)对 `send_demo_alert` 开启**人工审批**,对话时 Agent 调用前会弹出确认。
|
||||
|
||||
## 4. 建议试的问题
|
||||
|
||||
在 Agent 对话里问:
|
||||
|
||||
- 「调用 MCP 工具查一下智能家居中控保修多久」→ 应触发 `lookup_policy`
|
||||
- 「研发部 POC 负责人是谁」→ `lookup_policy` 或 `list_team_contacts`
|
||||
- 「现在 MCP Demo 服务器几点」→ `server_time`
|
||||
|
||||
若同时导入了 `website-docs/sample-data/` 里的文档,可以对比 **知识库检索答案** 与 **MCP 工具返回** 是否一致。
|
||||
|
||||
## 5. 注意事项
|
||||
|
||||
- WeKnora UI **不支持 stdio** 传输;必须用 **HTTP Streamable** 或 **SSE**。
|
||||
- Demo 只绑定 `127.0.0.1`,不要暴露到公网。
|
||||
- `send_demo_alert` 不会真正发送消息,仅返回模拟结果。
|
||||
|
||||
## 6. SSE 模式(可选)
|
||||
|
||||
```bash
|
||||
MCP_TRANSPORT=sse MCP_PORT=8011 ./start.sh
|
||||
```
|
||||
|
||||
WeKnora 里传输选 **SSE**,URL 填 `http://127.0.0.1:8011/sse`。
|
||||
@@ -0,0 +1,4 @@
|
||||
mcp>=2,<3
|
||||
starlette>=0.27.0
|
||||
uvicorn>=0.24.0
|
||||
httpx>=0.27.0
|
||||
@@ -0,0 +1,262 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
WeKnora 本地 MCP Demo Server
|
||||
|
||||
最小可运行的外部 MCP 服务,用于在 WeKnora「设置 → MCP 服务」里测试客户端接入。
|
||||
默认以 Streamable HTTP 监听 http://127.0.0.1:8010/mcp
|
||||
|
||||
启动:
|
||||
export MCP_SERVER_AUTH_TOKEN=weknora-demo-token
|
||||
python server.py
|
||||
|
||||
WeKnora 配置:
|
||||
传输:HTTP Streamable
|
||||
URL:http://127.0.0.1:8010/mcp
|
||||
认证:Bearer,令牌与 MCP_SERVER_AUTH_TOKEN 一致
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from mcp.server import MCPServer
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
|
||||
logger = logging.getLogger("mcp-demo")
|
||||
|
||||
mcp = MCPServer("weknora-mcp-demo", version="0.1.0")
|
||||
|
||||
# 与 website-docs/sample-data/ 配套的演示语料,方便 Agent 调用后对照知识库答案。
|
||||
DEMO_POLICIES: dict[str, str] = {
|
||||
"warranty": "智能家居中控 Pro 整机保修 24 个月,电池类配件 12 个月;人为拆解、进水不在保修范围。",
|
||||
"offline_voice": "若语音走云端识别,断外网后仅支持 App 与本地触摸屏;配置本地语音包后可继续使用基础指令。",
|
||||
"device_limit": "个人版账号最多绑定 3 台中控;企业版按合同授权,默认 50 台。",
|
||||
"travel_hotel_tier1": "一线城市(北上广深)出差住宿报销上限 600 元/晚(含税)。",
|
||||
"travel_meal": "出差期间餐饮费不单独报销;一线城市差旅补贴 150 元/天。",
|
||||
"poc_owner": "售后知识库 POC 技术负责人是研发部张明,产品对接人是李薇,测试负责人是赵磊。",
|
||||
"poc_deadline": "售后知识库 POC 目标 2024-03-01 前完成内网演示。",
|
||||
"matter_cert": "固件 3.5 计划在 2024 年 3 月底前发布灰度,完成 Matter 1.2 认证。",
|
||||
}
|
||||
|
||||
DEMO_CONTACTS: list[dict[str, str]] = [
|
||||
{"name": "陈浩", "role": "产品总监", "department": "产品部"},
|
||||
{"name": "张明", "role": "知识库与 AI 模块负责人", "department": "研发部"},
|
||||
{"name": "李薇", "role": "产品运营", "department": "产品部"},
|
||||
{"name": "王雪", "role": "交互设计负责人", "department": "设计部"},
|
||||
{"name": "赵磊", "role": "测试经理", "department": "测试部"},
|
||||
]
|
||||
|
||||
|
||||
def network_transport_auth_token() -> str:
|
||||
return os.getenv("MCP_SERVER_AUTH_TOKEN", "").strip()
|
||||
|
||||
|
||||
def require_network_transport_auth(transport: str) -> str:
|
||||
token = network_transport_auth_token()
|
||||
if transport in ("sse", "http") and not token:
|
||||
logger.error(
|
||||
"MCP_SERVER_AUTH_TOKEN is required for %s transport. "
|
||||
"Example: export MCP_SERVER_AUTH_TOKEN=weknora-demo-token",
|
||||
transport,
|
||||
)
|
||||
sys.exit(1)
|
||||
return token
|
||||
|
||||
|
||||
class MCPAuthMiddleware:
|
||||
"""SSE / HTTP 传输的 Bearer 鉴权中间件。"""
|
||||
|
||||
def __init__(self, app, token: str):
|
||||
self.app = app
|
||||
self.token = token
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
if scope.get("type") != "http":
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
|
||||
headers = {
|
||||
k.decode("latin-1").lower(): v.decode("latin-1")
|
||||
for k, v in scope.get("headers", [])
|
||||
}
|
||||
provided = ""
|
||||
auth = headers.get("authorization", "")
|
||||
if auth.lower().startswith("bearer "):
|
||||
provided = auth[7:].strip()
|
||||
elif "x-mcp-auth-token" in headers:
|
||||
provided = headers["x-mcp-auth-token"]
|
||||
|
||||
if not provided or not secrets.compare_digest(provided, self.token):
|
||||
body = b'{"error":"unauthorized"}'
|
||||
await send(
|
||||
{
|
||||
"type": "http.response.start",
|
||||
"status": 401,
|
||||
"headers": [[b"content-type", b"application/json"]],
|
||||
}
|
||||
)
|
||||
await send({"type": "http.response.body", "body": body})
|
||||
return
|
||||
|
||||
await self.app(scope, receive, send)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def echo(message: str) -> dict[str, Any]:
|
||||
"""回显一条消息,用于验证 MCP 连通性。"""
|
||||
return {"echo": message}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def add(a: float, b: float) -> dict[str, Any]:
|
||||
"""计算两个数字之和。"""
|
||||
return {"a": a, "b": b, "sum": a + b}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def server_time() -> dict[str, str]:
|
||||
"""返回 MCP Demo 服务器当前 UTC 时间。"""
|
||||
now = datetime.now(timezone.utc)
|
||||
return {
|
||||
"iso": now.isoformat(),
|
||||
"unix": str(int(now.timestamp())),
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def lookup_policy(topic: str) -> dict[str, Any]:
|
||||
"""查询演示政策/项目信息。topic 可用 warranty/offline_voice/device_limit/travel_hotel_tier1/travel_meal/poc_owner/poc_deadline/matter_cert,或中文关键词如「保修」「报销」「POC」。"""
|
||||
key = topic.strip().lower().replace(" ", "_")
|
||||
aliases = {
|
||||
"保修": "warranty",
|
||||
"质保": "warranty",
|
||||
"离线": "offline_voice",
|
||||
"语音": "offline_voice",
|
||||
"设备数": "device_limit",
|
||||
"住宿": "travel_hotel_tier1",
|
||||
"报销": "travel_hotel_tier1",
|
||||
"餐饮": "travel_meal",
|
||||
"补贴": "travel_meal",
|
||||
"负责人": "poc_owner",
|
||||
"张明": "poc_owner",
|
||||
"poc": "poc_owner",
|
||||
"验收": "poc_deadline",
|
||||
"matter": "matter_cert",
|
||||
"认证": "matter_cert",
|
||||
}
|
||||
for alias, mapped in aliases.items():
|
||||
if alias in topic:
|
||||
key = mapped
|
||||
break
|
||||
|
||||
if key in DEMO_POLICIES:
|
||||
return {"topic": key, "answer": DEMO_POLICIES[key], "source": "mcp-demo/static"}
|
||||
|
||||
matches = {
|
||||
k: v
|
||||
for k, v in DEMO_POLICIES.items()
|
||||
if key in k or any(ch in k for ch in key if len(key) >= 2)
|
||||
}
|
||||
if len(matches) == 1:
|
||||
only_key = next(iter(matches))
|
||||
return {"topic": only_key, "answer": matches[only_key], "source": "mcp-demo/static"}
|
||||
|
||||
return {
|
||||
"topic": topic,
|
||||
"available_topics": sorted(DEMO_POLICIES.keys()),
|
||||
"hint": "传入 topic 为上述键名,或中文关键词如「保修」「报销」「POC」。",
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def list_team_contacts(department: str = "") -> dict[str, Any]:
|
||||
"""列出演示项目团队成员;可按部门名过滤(产品部 / 研发部 / 设计部 / 测试部)。"""
|
||||
rows = DEMO_CONTACTS
|
||||
if department.strip():
|
||||
needle = department.strip()
|
||||
rows = [c for c in rows if needle in c["department"]]
|
||||
return {"count": len(rows), "contacts": rows}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def send_demo_alert(channel: str, message: str) -> dict[str, Any]:
|
||||
"""模拟向外部渠道发送通知(演示用,不会真正外发)。
|
||||
|
||||
适合在 WeKnora 里测试 MCP 工具人工审批:建议把此工具标记为需要审批。
|
||||
"""
|
||||
return {
|
||||
"ok": True,
|
||||
"simulated": True,
|
||||
"channel": channel,
|
||||
"message": message,
|
||||
"sent_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
|
||||
async def run_http(host: str, port: int) -> None:
|
||||
auth_token = require_network_transport_auth("http")
|
||||
try:
|
||||
import uvicorn
|
||||
except ImportError as e:
|
||||
raise ImportError("HTTP transport requires: pip install starlette uvicorn") from e
|
||||
|
||||
starlette_app = MCPAuthMiddleware(
|
||||
mcp.streamable_http_app(host=host, stateless_http=True),
|
||||
auth_token,
|
||||
)
|
||||
logger.info("Streamable HTTP MCP demo listening on http://%s:%d/mcp", host, port)
|
||||
config = uvicorn.Config(starlette_app, host=host, port=port, log_level="info")
|
||||
server = uvicorn.Server(config)
|
||||
await server.serve()
|
||||
|
||||
|
||||
async def run_sse(host: str, port: int) -> None:
|
||||
auth_token = require_network_transport_auth("sse")
|
||||
try:
|
||||
import uvicorn
|
||||
except ImportError as e:
|
||||
raise ImportError("SSE transport requires: pip install starlette uvicorn") from e
|
||||
|
||||
starlette_app = MCPAuthMiddleware(
|
||||
mcp.sse_app(host=host, message_path="/sse/messages/"),
|
||||
auth_token,
|
||||
)
|
||||
logger.info("SSE MCP demo listening on http://%s:%d/sse", host, port)
|
||||
config = uvicorn.Config(starlette_app, host=host, port=port, log_level="info")
|
||||
server = uvicorn.Server(config)
|
||||
await server.serve()
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="WeKnora local MCP demo server")
|
||||
parser.add_argument(
|
||||
"--transport",
|
||||
choices=["http", "sse"],
|
||||
default=os.getenv("MCP_TRANSPORT", "http"),
|
||||
help="Network transport (default: http / Streamable HTTP)",
|
||||
)
|
||||
parser.add_argument("--host", default=os.getenv("MCP_HOST", "127.0.0.1"))
|
||||
parser.add_argument("--port", type=int, default=int(os.getenv("MCP_PORT", "8010")))
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
args = parse_args()
|
||||
if args.transport == "http":
|
||||
await run_http(args.host, args.port)
|
||||
else:
|
||||
await run_sse(args.host, args.port)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
logger.info("stopped")
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
export MCP_SERVER_AUTH_TOKEN="${MCP_SERVER_AUTH_TOKEN:-weknora-demo-token}"
|
||||
export MCP_HOST="${MCP_HOST:-127.0.0.1}"
|
||||
export MCP_PORT="${MCP_PORT:-8010}"
|
||||
export MCP_TRANSPORT="${MCP_TRANSPORT:-http}"
|
||||
|
||||
VENV_DIR=".venv"
|
||||
if [[ ! -d "$VENV_DIR" ]]; then
|
||||
echo "Creating virtualenv in $VENV_DIR ..."
|
||||
python3 -m venv "$VENV_DIR"
|
||||
fi
|
||||
# shellcheck disable=SC1091
|
||||
source "$VENV_DIR/bin/activate"
|
||||
|
||||
if ! python -c "import mcp" 2>/dev/null; then
|
||||
echo "Installing dependencies..."
|
||||
pip install -r requirements.txt
|
||||
fi
|
||||
|
||||
echo "MCP Demo"
|
||||
echo " transport : ${MCP_TRANSPORT}"
|
||||
echo " endpoint : http://${MCP_HOST}:${MCP_PORT}/$([ "$MCP_TRANSPORT" = http ] && echo mcp || echo sse)"
|
||||
echo " auth token: ${MCP_SERVER_AUTH_TOKEN}"
|
||||
echo
|
||||
echo "WeKnora UI → 设置 → MCP 服务 → 新建"
|
||||
echo " 传输: HTTP Streamable"
|
||||
echo " URL : http://${MCP_HOST}:${MCP_PORT}/mcp"
|
||||
echo " 认证: Bearer / ${MCP_SERVER_AUTH_TOKEN}"
|
||||
echo
|
||||
|
||||
exec python server.py --transport "$MCP_TRANSPORT" --host "$MCP_HOST" --port "$MCP_PORT"
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""列出 MCP Demo 暴露的工具(需先启动 server.py)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
import httpx
|
||||
from mcp import ClientSession
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
|
||||
|
||||
async def main() -> int:
|
||||
base = os.getenv("MCP_DEMO_URL", "http://127.0.0.1:8010/mcp")
|
||||
token = os.getenv("MCP_SERVER_AUTH_TOKEN", "weknora-demo-token")
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=30.0,
|
||||
) as http_client:
|
||||
async with streamable_http_client(base, http_client=http_client) as (read, write):
|
||||
async with ClientSession(read, write) as session:
|
||||
await session.initialize()
|
||||
tools = await session.list_tools()
|
||||
print(f"connected: {base}")
|
||||
print(f"tools ({len(tools.tools)}):")
|
||||
for tool in tools.tools:
|
||||
print(f" - {tool.name}: {tool.description}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(asyncio.run(main()))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f"failed: {exc}", file=sys.stderr)
|
||||
print("start the server first: ./start.sh", file=sys.stderr)
|
||||
raise SystemExit(1) from exc
|
||||
@@ -0,0 +1,92 @@
|
||||
# Skills 示例
|
||||
|
||||
本目录包含 Agent Skills 功能的示例。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
skills/
|
||||
├── README.md # 本文件
|
||||
└── pdf-processing/ # PDF 处理技能示例
|
||||
├── SKILL.md # 主文件(Level 2)
|
||||
├── FORMS.md # 补充文档(Level 3)
|
||||
└── scripts/ # 可执行脚本
|
||||
├── analyze_form.py
|
||||
└── extract_text.py
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 运行 Demo
|
||||
|
||||
```bash
|
||||
go run ./cmd/skills-demo/main.go
|
||||
```
|
||||
|
||||
### 创建新 Skill
|
||||
|
||||
1. 在本目录创建新文件夹:
|
||||
|
||||
```bash
|
||||
mkdir my-new-skill
|
||||
```
|
||||
|
||||
2. 创建 `SKILL.md`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: my-new-skill
|
||||
description: Description of what this skill does and when to use it.
|
||||
---
|
||||
|
||||
# My New Skill
|
||||
|
||||
Instructions for the agent...
|
||||
```
|
||||
|
||||
3. 添加脚本(可选):
|
||||
|
||||
```bash
|
||||
mkdir my-new-skill/scripts
|
||||
# 添加你的脚本
|
||||
```
|
||||
|
||||
## 详细文档
|
||||
|
||||
完整文档请参阅:[Agent Skills 文档](../../docs/agent-skills.md)
|
||||
|
||||
## 示例:pdf-processing
|
||||
|
||||
这是一个功能完整的示例技能,展示了:
|
||||
|
||||
- **SKILL.md**: 包含 YAML frontmatter 的主文件
|
||||
- **FORMS.md**: 补充参考文档
|
||||
- **scripts/**: 可在沙箱中执行的 Python 脚本
|
||||
|
||||
### 技能描述
|
||||
|
||||
```yaml
|
||||
name: pdf-processing
|
||||
description: Extract text and tables from PDF files, fill forms, merge documents.
|
||||
```
|
||||
|
||||
### 包含的脚本
|
||||
|
||||
| 脚本 | 功能 |
|
||||
|------|------|
|
||||
| `analyze_form.py` | 分析 PDF 表单字段 |
|
||||
| `extract_text.py` | 从 PDF 提取文本 |
|
||||
|
||||
### 使用示例
|
||||
|
||||
Agent 会根据用户请求自动调用:
|
||||
|
||||
```
|
||||
用户: "分析一下这个 PDF 表单有哪些字段"
|
||||
|
||||
Agent:
|
||||
1. 识别匹配 pdf-processing 技能
|
||||
2. 调用 read_skill 加载技能内容
|
||||
3. 调用 execute_skill_script 执行 analyze_form.py
|
||||
4. 返回表单字段分析结果
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
# PDF Form Filling Guide
|
||||
|
||||
This guide covers how to fill PDF forms programmatically.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Install required packages:
|
||||
```bash
|
||||
pip install pypdf pdfrw
|
||||
```
|
||||
|
||||
## Basic Form Filling
|
||||
|
||||
```python
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
def fill_form(input_path, output_path, field_data):
|
||||
reader = PdfReader(input_path)
|
||||
writer = PdfWriter()
|
||||
|
||||
# Clone the original PDF
|
||||
writer.clone_document_from_reader(reader)
|
||||
|
||||
# Fill form fields
|
||||
for page in writer.pages:
|
||||
writer.update_page_form_field_values(page, field_data)
|
||||
|
||||
# Save the filled PDF
|
||||
with open(output_path, "wb") as f:
|
||||
writer.write(f)
|
||||
```
|
||||
|
||||
## Supported Field Types
|
||||
|
||||
- Text fields
|
||||
- Checkboxes
|
||||
- Radio buttons
|
||||
- Dropdown lists
|
||||
|
||||
## Tips
|
||||
|
||||
1. Use `scripts/analyze_form.py` to discover available fields
|
||||
2. Field names are case-sensitive
|
||||
3. Always verify output after filling
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: pdf-processing
|
||||
description: Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction.
|
||||
---
|
||||
# PDF Processing
|
||||
|
||||
This skill provides utilities for working with PDF documents.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Use pdfplumber to extract text from PDFs:
|
||||
|
||||
```python
|
||||
import pdfplumber
|
||||
|
||||
with pdfplumber.open("document.pdf") as pdf:
|
||||
text = pdf.pages[0].extract_text()
|
||||
print(text)
|
||||
```
|
||||
|
||||
## Available Operations
|
||||
|
||||
1. **Text Extraction**: Extract text content from PDF pages
|
||||
2. **Table Extraction**: Extract tabular data from PDFs
|
||||
3. **Form Filling**: Fill PDF forms with provided data
|
||||
4. **Document Merging**: Combine multiple PDFs into one
|
||||
|
||||
## Advanced Features
|
||||
|
||||
**Form filling**: See [FORMS.md](FORMS.md) for complete guide
|
||||
|
||||
**Utility scripts**:
|
||||
- Run `scripts/analyze_form.py` to extract form fields
|
||||
- Run `scripts/extract_text.py` to extract text from a PDF
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Always validate PDF files before processing
|
||||
2. Handle password-protected PDFs gracefully
|
||||
3. Check for scanned PDFs that may require OCR
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Analyze PDF form fields and output their structure.
|
||||
Usage: python analyze_form.py <pdf_file>
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
|
||||
def analyze_form(pdf_path):
|
||||
"""Analyze form fields in a PDF file."""
|
||||
# This is a mock implementation for testing
|
||||
# In production, would use pypdf or pdfrw
|
||||
|
||||
print(f"Analyzing PDF: {pdf_path}")
|
||||
print("=" * 50)
|
||||
|
||||
# Mock form fields for demonstration
|
||||
fields = {
|
||||
"name": {"type": "text", "required": True},
|
||||
"email": {"type": "text", "required": True},
|
||||
"date": {"type": "date", "required": False},
|
||||
"agree_terms": {"type": "checkbox", "required": True},
|
||||
"signature": {"type": "signature", "required": True}
|
||||
}
|
||||
|
||||
print("\nDiscovered Form Fields:")
|
||||
print("-" * 30)
|
||||
for field_name, props in fields.items():
|
||||
required_str = "[REQUIRED]" if props["required"] else "[optional]"
|
||||
print(f" {field_name}: {props['type']} {required_str}")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("Analysis complete.")
|
||||
|
||||
# Output JSON for programmatic use
|
||||
return json.dumps(fields, indent=2)
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python analyze_form.py <pdf_file>")
|
||||
sys.exit(1)
|
||||
|
||||
result = analyze_form(sys.argv[1])
|
||||
print("\nJSON Output:")
|
||||
print(result)
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Extract text from PDF files.
|
||||
Usage: python extract_text.py <pdf_file> [--page N]
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
def extract_text(pdf_path, page_num=None):
|
||||
"""Extract text from a PDF file."""
|
||||
# This is a mock implementation for testing
|
||||
# In production, would use pdfplumber or pypdf
|
||||
|
||||
print(f"Extracting text from: {pdf_path}")
|
||||
|
||||
if page_num:
|
||||
print(f"Page: {page_num}")
|
||||
else:
|
||||
print("All pages")
|
||||
|
||||
print("=" * 50)
|
||||
|
||||
# Mock extracted text
|
||||
mock_text = """
|
||||
Sample PDF Document
|
||||
|
||||
This is a demonstration of text extraction from PDF files.
|
||||
|
||||
Key Features:
|
||||
- Fast and efficient text extraction
|
||||
- Preserves document structure
|
||||
- Handles multi-page documents
|
||||
|
||||
For more information, visit our documentation.
|
||||
"""
|
||||
|
||||
print(mock_text)
|
||||
print("=" * 50)
|
||||
print("Extraction complete.")
|
||||
|
||||
return mock_text.strip()
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python extract_text.py <pdf_file> [--page N]")
|
||||
sys.exit(1)
|
||||
|
||||
pdf_path = sys.argv[1]
|
||||
page_num = None
|
||||
|
||||
if len(sys.argv) > 3 and sys.argv[2] == "--page":
|
||||
page_num = int(sys.argv[3])
|
||||
|
||||
extract_text(pdf_path, page_num)
|
||||
Reference in New Issue
Block a user