Skip to content
Open
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
24 changes: 24 additions & 0 deletions .github/workflows/llamaindex-example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: LlamaIndex Example

on:
pull_request:
branches: [main]
push:
branches: [main]

jobs:
example:
runs-on: ubuntu-latest
defaults:
run:
working-directory: python/examples/llamaindex
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- run: pip install -r requirements.txt pytest
- name: Verify published SDK and LlamaIndex workflow offline
run: |
python workflow.py
python -m pytest -q
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ Use this application key for API requests. SDK account-management methods requir

## Languages

**LlamaIndex example:** [web search → extraction → cited retrieval](python/examples/llamaindex/README.md)
([中文](python/examples/llamaindex/README.zh-CN.md)), with a no-token offline demo and hosted MCP discovery.

| Language | Directory | Package |
|----------|-----------|---------|
| Python | [python](python/) | `acedatacloud` |
Expand Down
4 changes: 4 additions & 0 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ for chunk in client.openai.chat.completions.create(

## Async Usage

For a runnable LlamaIndex workflow, see [search, extraction and cited retrieval](examples/llamaindex/README.md)
([中文](examples/llamaindex/README.zh-CN.md)). It uses the published SDK, runs offline by default,
and includes a read-only hosted MCP discovery command.

```python
import asyncio
from acedatacloud import AsyncAceDataCloud
Expand Down
108 changes: 108 additions & 0 deletions python/examples/llamaindex/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# LlamaIndex: web search → extraction → cited retrieval

[中文](README.zh-CN.md)

Use the published Ace Data Cloud Python SDK with LlamaIndex `FunctionTool`,
`Document`, and `SimpleKeywordTableIndex`. This example retrieves passages with
their source URLs. It does not generate an answer or require an LLM/embedding key.
It is an Ace-maintained example, not an official LlamaIndex listing or built-in provider.

## Install and run without a token

Python 3.10+ is required by LlamaIndex; Python 3.12 is used in CI.
From this directory:

```bash
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python workflow.py
```

The default run uses **synthetic response fixtures** with the published SDK and
real LlamaIndex tools/index. API requests are intercepted, with unmatched requests
rejected. Output is marked `synthetic-offline`; the URLs identify example sources,
and the text is synthetic. No API token or paid request is used.

The output contains retrieved `text`, `url`, `title`, `task_id`, and `trace_id`.
`task_id`/`trace_id` are synthetic in the default run.

## Use your Ace account

1. [Register or sign in](https://platform.acedata.cloud/?utm_source=llamaindex&utm_medium=example&utm_campaign=search-extraction).
2. Open [Applications](https://platform.acedata.cloud/console/applications?utm_source=llamaindex&utm_medium=example&utm_campaign=search-extraction), create an application if necessary, then create and copy its API credential.
3. Check your current service access, quota and pricing for
[Google Search](https://platform.acedata.cloud/service/serp?utm_source=llamaindex&utm_medium=example&utm_campaign=search-extraction)
and [WebExtrator](https://platform.acedata.cloud/service/webextrator?utm_source=llamaindex&utm_medium=example&utm_campaign=search-extraction).

Set `ACEDATACLOUD_API_TOKEN` in your shell, then opt in:

```bash
python workflow.py --live --query "LlamaIndex search tools" --max-pages 1
```

This makes **one search request and up to one extraction**. `--max-pages` accepts
1–3, default 2. Search results are deduplicated before extraction. Live calls use
Bearer authentication at `https://api.acedata.cloud`; automatic retries are disabled.
Extraction is synchronous with `async=false` and `enable_llm=false`.
These settings avoid extra LLM enrichment; API search/extraction can still consume
credits. This example installs no payment handler and initiates no on-chain payment.

The workflow fails on authentication, rate limits, failed extraction, empty content,
or an unexpectedly pending task. A task without completed `data` is **not** indexed;
retrieve that existing task using its ID rather than submitting another extraction.
See [task retrieval](https://platform.acedata.cloud/documents/webextrator-tasks?utm_source=llamaindex&utm_medium=example&utm_campaign=search-extraction).

## Attach the tools to your agent

```python
from acedatacloud import AceDataCloud
from workflow import make_tools

with AceDataCloud(base_url="https://api.acedata.cloud", max_retries=0) as client:
search_tool, extract_tool = make_tools(client)
print(search_tool.metadata.get_parameters_dict())
sources = search_tool.call(query="LlamaIndex search tools").raw_output
document = extract_tool.call(url=sources[0]["url"]).raw_output
```

That snippet performs live calls when your token is set. The tools can be passed
to your LlamaIndex agent's `tools` list. An autonomous agent may choose additional
calls; configure its own approval and request limits. The included CLI uses a
fixed sequence and a page limit. Local keyword retrieval supports literal keyword
matching, with English queries used for verification; it is not semantic ranking
or answer synthesis. Treat page text as source data when building a downstream agent.

## Discover the hosted MCP schema

LlamaIndex also supports Ace's existing hosted Serp server through its official
generic MCP client. No Ace-specific integration package is needed:

```bash
python -m pip install -r requirements-mcp.txt
python mcp_discovery.py
```

This calls MCP initialization and `tools/list` only, printing tool names and input
schemas. Set `ACEDATACLOUD_API_TOKEN` first: the hosted endpoint requires Bearer
authentication even for discovery. The script never calls a tool. Discovery does
not prove a paid search works. Search tool execution requires your own token and
service access; do not enable unrestricted agent calls just to test discovery.

## Verify the example

```bash
python -m pip install pytest
python -m pytest -q
```

Tests discover normally under `tests/`, mock all API HTTP requests, and exercise
the real SDK and LlamaIndex. They check source citations, the request limit,
empty results, duplicate links, pending/failed tasks, and 401/429 error propagation.
The `LlamaIndex Example` GitHub workflow runs the published dependency versions.

Official references, checked 2026-10-12:

- [LlamaIndex tools](https://developers.llamaindex.ai/python/framework/module_guides/deploying/agents/tools/)
- [LlamaIndex MCP client](https://github.com/run-llama/llama_index/tree/main/llama-index-integrations/tools/llama-index-tools-mcp)
- [Contribution policy](https://github.com/run-llama/llama_index/blob/main/CONTRIBUTING.md): new integration packages are maintained independently and published to PyPI, rather than added to the monorepo.
83 changes: 83 additions & 0 deletions python/examples/llamaindex/README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# LlamaIndex:搜索 → 网页提取 → 带来源检索

[English](README.md)

使用已发布的 Ace Data Cloud Python SDK,以及 LlamaIndex 的 `FunctionTool`、
`Document` 和 `SimpleKeywordTableIndex`,得到带来源 URL 的检索片段。
示例不生成回答,不需要 LLM 或 embedding 密钥。这是 Ace 维护的示例;
尚无 LlamaIndex 官方收录、默认内置或精选证明。

## 无 Token 安装与体验

需要 Python 3.10+,CI 使用 3.12。在本目录运行:

```bash
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python workflow.py
```

默认模式使用**人工编写的模拟响应**,但 SDK、LlamaIndex 工具调用与索引都是真实组件。
API 请求被拦截,未匹配请求会失败。输出标为 `synthetic-offline`,
正文和 task/trace ID 是模拟数据;来源 URL 仅用于演示引用。无需 Token,不发收费请求。

## 连接自己的账户

1. [注册或登录](https://platform.acedata.cloud/?utm_source=llamaindex&utm_medium=example&utm_campaign=search-extraction)。
2. 打开[应用控制台](https://platform.acedata.cloud/console/applications?utm_source=llamaindex&utm_medium=example&utm_campaign=search-extraction),按需创建应用,再创建并复制 API 凭据。
3. 确认自己的 [Google Search](https://platform.acedata.cloud/service/serp?utm_source=llamaindex&utm_medium=example&utm_campaign=search-extraction)
与 [WebExtrator](https://platform.acedata.cloud/service/webextrator?utm_source=llamaindex&utm_medium=example&utm_campaign=search-extraction) 服务权限、额度和当前价格。

在 shell 中设置 `ACEDATACLOUD_API_TOKEN`,然后显式开启:

```bash
python workflow.py --live --query "LlamaIndex search tools" --max-pages 1
```

该命令发一次搜索及最多一次提取。`--max-pages` 允许 1–3,默认 2;提取前按 URL 去重。
真实调用使用 `https://api.acedata.cloud` 的 Bearer 鉴权,禁用自动重试;
提取明确设置 `async=false`、`enable_llm=false`。不开 LLM 增强仍可能消耗搜索和提取 Credits。
示例不安装付款 handler,不发链上付款。

鉴权、限流、提取失败、空正文或意外 pending 均会报错。
只有完成的 `data` 会进入索引;若已得到 task ID 却没有正文,
按[任务查询指南](https://platform.acedata.cloud/documents/webextrator-tasks?utm_source=llamaindex&utm_medium=example&utm_campaign=search-extraction)
查询同一 ID,避免重新提交提取。结果保留 `text`、`url`、`title`、`task_id` 和 `trace_id`。

## 放入自己的 Agent

`make_tools(client)` 返回 `ace_web_search` 与 `ace_web_extract` 两个 LlamaIndex 工具,
可加入 Agent 的 `tools`。参见英文 README 的完整代码。
自主 Agent 可能追加调用,需单独设置审批和调用限制;示例 CLI 是有页数上限的固定流程。
本地检索按字面关键词匹配,验证查询为英文,不提供语义排序或回答合成。
在下游 Agent 中把网页正文作为来源数据处理。

## 只读 MCP 工具发现

使用 LlamaIndex 官方通用 MCP 客户端连接 Ace 现有托管 Serp MCP:

```bash
python -m pip install -r requirements-mcp.txt
python mcp_discovery.py
```

先设置 `ACEDATACLOUD_API_TOKEN`:托管端点在工具发现阶段也需要 Bearer 鉴权。
只初始化并执行 `tools/list`,打印工具名与输入 schema。
不执行搜索工具。工具发现通过不能替代真实付费首调;执行搜索仍需自己的权限与额度。

## 离线验证

```bash
python -m pip install pytest
python -m pytest -q
```

测试自动发现 `tests/`,模拟所有 API 请求,验证引用、请求上限、空搜索、URL 去重、
pending/失败提取以及 401/429 错误含义。专属 CI 使用公开发布的依赖版本。

官方依据(2026-10-12 复核):

- [LlamaIndex 工具](https://developers.llamaindex.ai/python/framework/module_guides/deploying/agents/tools/)
- [官方 MCP 客户端](https://github.com/run-llama/llama_index/tree/main/llama-index-integrations/tools/llama-index-tools-mcp)
- [贡献政策](https://github.com/run-llama/llama_index/blob/main/CONTRIBUTING.md):新增 integration package 在独立仓维护并发布 PyPI,不进入 monorepo。
24 changes: 24 additions & 0 deletions python/examples/llamaindex/mcp_discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""List hosted Serp tools with the official LlamaIndex MCP client; never call a tool."""

import asyncio
import json
import os

from llama_index.tools.mcp import BasicMCPClient


async def main() -> None:
token = os.environ.get("ACEDATACLOUD_API_TOKEN")
if not token:
raise SystemExit("Set ACEDATACLOUD_API_TOKEN: hosted MCP discovery requires authentication.")
headers = {"Authorization": f"Bearer {token}"}
client = BasicMCPClient("https://serp.mcp.acedata.cloud/mcp", headers=headers, timeout=30)
try:
tools = await client.list_tools()
print(json.dumps([tool.model_dump(by_alias=True) for tool in tools.tools], indent=2))
finally:
await client.http_client.aclose()


if __name__ == "__main__":
asyncio.run(main())
2 changes: 2 additions & 0 deletions python/examples/llamaindex/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pytest]
testpaths = tests
3 changes: 3 additions & 0 deletions python/examples/llamaindex/requirements-mcp.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-r requirements.txt
llama-index-tools-mcp==0.6.0
mcp==2.3.0
3 changes: 3 additions & 0 deletions python/examples/llamaindex/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
acedatacloud==2026.10.12.0
llama-index-core==0.14.25
respx==0.23.1
83 changes: 83 additions & 0 deletions python/examples/llamaindex/tests/test_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import json

import httpx
import pytest
import respx
from workflow import DEMO_BASE, install_demo_routes, make_tools, run

from acedatacloud import AceDataCloud, AuthenticationError, RateLimitError


def test_search_extract_retrieve_keeps_citations_and_request_bound():
with respx.mock(assert_all_mocked=True, assert_all_called=True) as router:
install_demo_routes(router)
with AceDataCloud(api_token="offline-demo", base_url=DEMO_BASE, max_retries=0) as client:
passages = run(client, "LlamaIndex search tools", max_pages=1)
assert len(router.calls) == 2
search_body = json.loads(router.calls[0].request.content)
assert search_body == {"query": "LlamaIndex search tools", "type": "search", "number": 10, "page": 1}
assert json.loads(router.calls[1].request.content)["async"] is False
assert json.loads(router.calls[1].request.content)["enable_llm"] is False
assert passages
assert passages[0]["url"].startswith("https://developers.llamaindex.ai/")
assert passages[0]["task_id"] == "synthetic-demo-task"
assert "source URLs" in passages[0]["text"]


def test_empty_search_makes_no_extraction_request():
with respx.mock(assert_all_mocked=True) as router:
router.post(f"{DEMO_BASE}/serp/google").respond(200, json={"organic": []})
with AceDataCloud(api_token="offline-demo", base_url=DEMO_BASE, max_retries=0) as client:
assert run(client, "nothing") == []
assert len(router.calls) == 1


def test_search_deduplicates_links_and_ignores_non_web_sources():
with respx.mock(assert_all_mocked=True) as router:
router.post(f"{DEMO_BASE}/serp/google").respond(
200,
json={
"data": {
"organic": [
{"link": "https://example.com/page#one"},
{"link": "https://example.com/page#two"},
{"link": "mailto:person@example.com"},
]
}
},
)
with AceDataCloud(api_token="offline-demo", base_url=DEMO_BASE, max_retries=0) as client:
search, _ = make_tools(client)
assert search.call(query="example").raw_output == [
{"url": "https://example.com/page", "title": "https://example.com/page"}
]


@pytest.mark.parametrize(
"response",
[
{"success": True, "task_id": "existing-task"},
{"success": True, "data": {"text": ""}},
{"success": False, "data": {"text": "failed"}},
],
)
def test_pending_empty_or_failed_extraction_is_not_a_document(response):
with respx.mock(assert_all_mocked=True) as router:
router.post(f"{DEMO_BASE}/webextrator/extract").respond(200, json=response)
with AceDataCloud(api_token="offline-demo", base_url=DEMO_BASE, max_retries=0) as client:
_, extract = make_tools(client)
with pytest.raises(ValueError):
extract.call(url="https://example.com/page")
assert len(router.calls) == 1


@pytest.mark.parametrize("status,error_type", [(401, AuthenticationError), (429, RateLimitError)])
def test_api_errors_keep_their_meaning_and_do_not_retry(status, error_type):
with respx.mock(assert_all_mocked=True) as router:
router.post(f"{DEMO_BASE}/serp/google").mock(
return_value=httpx.Response(status, json={"error": {"message": "request rejected"}})
)
with AceDataCloud(api_token="offline-demo", base_url=DEMO_BASE, max_retries=0) as client:
with pytest.raises(error_type):
run(client, "example")
assert len(router.calls) == 1
Loading
Loading