Metadata-Version: 2.5
Name: zleap-sag2
Version: 0.1.0
Summary: SAG2 passage retrieval with portable relational storage
Project-URL: Homepage, https://github.com/Zleap-AI/zleap
Project-URL: Repository, https://github.com/Zleap-AI/zleap
Project-URL: Documentation, https://github.com/Zleap-AI/zleap/tree/main/packages/sag2#readme
Project-URL: Issues, https://github.com/Zleap-AI/zleap/issues
License-Expression: MIT
License-File: LICENSE
License-File: NOTICE
Requires-Python: >=3.11
Requires-Dist: aiohttp>=3.14.3
Requires-Dist: aiosqlite>=0.19
Requires-Dist: jieba==0.42.1
Requires-Dist: pydantic-settings>=2.6
Requires-Dist: pydantic>=2.5
Requires-Dist: pyuca==1.2
Requires-Dist: regex>=2026.9.29
Requires-Dist: sqlalchemy[asyncio]>=2.0
Requires-Dist: tiktoken>=0.14
Requires-Dist: tokenizers>=0.22
Requires-Dist: tzdata
Provides-Extra: all
Requires-Dist: aiomysql>=0.2; extra == 'all'
Requires-Dist: asyncpg>=0.29; extra == 'all'
Requires-Dist: cryptography>=50; extra == 'all'
Requires-Dist: markitdown[xlsx]<0.2,>=0.1.5; extra == 'all'
Requires-Dist: openpyxl<4,>=3.1.5; extra == 'all'
Requires-Dist: pandas<3,>=2.2; extra == 'all'
Requires-Dist: xlrd<3,>=2; extra == 'all'
Provides-Extra: dev
Requires-Dist: build>=1; extra == 'dev'
Requires-Dist: hatchling>=1.27; extra == 'dev'
Requires-Dist: httpx>=0.28; extra == 'dev'
Requires-Dist: packaging>=24; extra == 'dev'
Requires-Dist: pytest-asyncio>=1; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff==0.16.4; extra == 'dev'
Requires-Dist: twine>=6; extra == 'dev'
Provides-Extra: mysql
Requires-Dist: aiomysql>=0.2; extra == 'mysql'
Requires-Dist: cryptography>=50; extra == 'mysql'
Provides-Extra: postgres
Requires-Dist: asyncpg>=0.29; extra == 'postgres'
Provides-Extra: tables
Requires-Dist: markitdown[xlsx]<0.2,>=0.1.5; extra == 'tables'
Requires-Dist: openpyxl<4,>=3.1.5; extra == 'tables'
Requires-Dist: pandas<3,>=2.2; extra == 'tables'
Requires-Dist: xlrd<3,>=2; extra == 'tables'
Description-Content-Type: text/markdown

# zleap-sag2

SAG2 原文检索引擎：统一词法召回、多轮 JEV 证据评分和本地多样性排序。
支持 SQLite、MySQL、PostgreSQL 和 OceanBase。连接已有 SAG1 库时，自动从
`source_chunk` 读取现有段落；空库使用 SAG2 自有存储。

发布到 PyPI 后，可按包名安装：

```sh
pip install zleap-sag2
# MySQL / OceanBase
pip install 'zleap-sag2[mysql]'
# PostgreSQL
pip install 'zleap-sag2[postgres]'
# 电子表格转换
pip install 'zleap-sag2[tables]'
```

从源码安装时，先进入包含本文件和 `pyproject.toml` 的包目录，再执行
`pip install .`；可选依赖使用 `pip install '.[tables]'` 等形式。
也可通过 `pip install` 指定已下载 wheel 文件的实际路径。

### 独立开发

可以直接在 IDE 或 Codex 中打开包目录：仓库中的 `packages/sag2`，或解压后的
源码包根目录。以下命令均在该目录执行。源码、测试、脚本、文档和环境变量模板
均在本目录内；使用仓库检出时，Git 提交仍属于 `zleap` 根仓库。

```sh
make install                    # 本目录 .venv，只安装 SAG2 和开发/表格依赖
make verify                     # lint、离线测试、构建、产物元数据检查
cp .env.example .env             # 需要连接模型服务时再填写
```

也可在根目录运行 `make -C packages/sag2 verify`。根目录的 `make verify` 仍检查 SAG。
本包独立版本为 `src/zleap/sag2/__about__.py`，发布 tag 为 `zleap-sag2/vX.Y.Z`；
发布配置见仓库的 [RELEASING.md](https://github.com/Zleap-AI/zleap/blob/main/RELEASING.md)。`zleap` 元包暂不依赖 SAG2，
使用者需单独安装 `zleap-sag2`。

```python
import asyncio
from zleap.sag2 import DataEngine, EngineConfig
from zleap.sag2 import TextSource, SourceDescriptor, SearchScope, SearchRequest


async def main():
    config = EngineConfig.from_env(".env")
    async with DataEngine(config, data_source_id="demo") as engine:
        await engine.ingest(
            TextSource(
                text="# 示例\nAlice 出生在北京。",
                descriptor=SourceDescriptor(
                    data_source_id="demo",
                    source_id="alice",
                    source_type="text",
                ),
            )
        )
        result = await engine.search(
            SearchRequest(query="Alice 出生在哪里？", scope=SearchScope(data_source_ids=("demo",)))
        )
        for hit in result.chunks:
            print(hit.score, hit.content)


asyncio.run(main())
```

`parse` 和 `chunk` 可在启动数据库前使用。`index(ChunkSet)` 直接保存已有分块。

### 配置

关系库环境变量与 SAG1 一致，使用 `EngineConfig.from_env(".env")` 显式读取文件；
`EngineConfig.from_env()` 只读取进程环境，不自动寻找 `.env`。最小配置：

```dotenv
SAG_RELATIONAL_PROVIDER=sqlite
SAG_DATA_DIR=./.zleap
DECISION_ENDPOINT=https://api.typesafe.ai/v1/systemone
DECISION_MODEL=jev-latest
DECISION_API_KEY=your-key
```

推荐使用 [JEV（TypeSafe）](https://typesafe.ai/) 或
[D1-ultra（Liquid AI）](https://www.liquid.ai/blog/d1-ultra) 作为决策模型。
使用 D1-ultra 时，将接口改为 `https://api.liquid.ai/decisions/v1/systemone`、
模型改为 `d1-ultra`，并填写 Liquid AI 的 API Key。使用其他服务商时，
模型 ID 与接口地址以该服务商为准，三项配置需对应同一服务。

决策请求的 token 预算使用随包提供的 `o200k_base` 编码资源；首次启动无需下载
tokenizer 或预置缓存。实际用量仍取决策服务返回的 `usage`。

SQLite 使用 `<SAG_DATA_DIR>/sag.db`，默认 `.zleap/sag.db`。服务库使用
`SAG_RELATIONAL_HOST` / `PORT` / `USER` / `PASSWORD` / `DATABASE`，连接池、超时及
PostgreSQL TLS 参数同样沿用 `SAG_RELATIONAL_*`。默认端口为 MySQL 3306、
PostgreSQL 5432、OceanBase 2881。

`RELATIONAL_PROVIDER`、`DATA_DIR`、`DB_HOST` / `DB_PORT` / `DB_USER` /
`DB_PASSWORD` / `DB_NAME`、`DB_POOL_*` / `DB_MAX_OVERFLOW` / `DB_CONNECT_TIMEOUT` /
`DB_COMMAND_TIMEOUT`、`DB_TLS_*` 也与 SAG1 一致。同名进程变量覆盖文件值；
同时设置等价名称时，`SAG_*` 优先，空值视为未设置。

SAG1 的配置文件可以直接复用，SAG2 不使用其中的 LLM、Embedding、向量库和
事件配置；搜索需补充 `DECISION_*`。未配置任何决策模型参数时仍可解析、切块、读取与续页，
部分配置决策模型参数会在加载时明确报错。也可以直接构造 `EngineConfig` 和
`RelationalConfig`，显式配置不读取环境变量。

### 直接搜索 SAG1 已有数据

用 `EngineConfig.from_env(".env")` 读取 SAG1 的连接配置，补充决策模型配置后直接调用
`search()` 或 `search_page()`，无需先调用 `index()`。
引擎启动时识别 `source_chunk`，沿用原段落 ID，读取 `article` / `chat_conversation`
补充来源信息，并遵守活动 generation、来源删除和数据源停用状态。normal / lite
的文本关系表均可读取，不需要安装 `zleap-sag` 或访问原向量库、事件表。

此模式支持搜索、分页、`read_chunk()`、`read_outline()` 和 `source_state()`。
原 SAG1 业务表保持只读，写入、删除和持久处理任务继续由 SAG1 管理；这些调用
在 SAG2 中会明确报 `sag.read_only_storage`，`capabilities()` 返回实际可用能力。
启动只补建 `sag2_state` / `sag2_search_sessions` 两张辅助表，需要相应建表权限；
翻页仍会写会话状态，SAG1/SAG2 的分页结果不会混用。

SAG1 的后续写入会使缓存刷新、旧游标过期，包括没有更新 manifest 的普通
`index()` 写入。为此，当前每次读取与游标校验都会扫描请求数据源的段落并计算
可见文本指纹，成本随范围内数据量增长。不提供旧 SAG2 数据向 SAG1 的迁移。

会话和录音转写支持按完整单元重叠切块。对 `parse()` 返回的会话或录音结果使用：

```python
from zleap.sag2 import ChunkOptions

chunk_set = await engine.chunk(
    parsed,
    ChunkOptions(strategy="overlap", max_tokens=800, overlap_tokens=100),
)
```

下一块会尽可能携带上一块末尾连续的完整消息或转写单元。重叠部分的 token 数
不超过 `overlap_tokens`，同时为新单元预留 `max_tokens` 内的空间；计算包含说话人
标签和换行。单元放不下时减少或取消重叠，不截断消息。单条超长单元仍完整保留，
因此可能单独超过 `max_tokens`。重叠单元保留原始文本、引用及时间范围。
`overlap_tokens` 默认是 0，表示不重叠；会话默认的时间窗口和录音默认的 token
窗口不受影响。

`SearchOptions` 配置 SAG2 候选数、扩展词数、追加轮数和输出数量；默认 100/20/2/10。
搜索失败会抛出异常，JEV 可重试错误最多尝试四次。

分页搜索沿用 `zleap-sag` 的调用方式：

```python
from zleap.sag2 import SearchPageRequest

# request 是包含 query、scope、options 的 SearchRequest。
page = await engine.search_page(SearchPageRequest(request=request, page_size=20))
for hit in page.result.chunks:
    print(hit.score, hit.content)
if page.next_cursor is not None:
    page = await engine.search_page(
        SearchPageRequest(cursor=page.next_cursor, cursor_request=request)
    )
```

首次执行 SAG2 并持久化结果，后续翻页不再调用 JEV，可跨进程与重启使用。
总数受搜索的 `top_k` 限制；默认每页 50 项、有效期 300 秒。
问题或范围不匹配、游标篡改会报 `CursorInvalidError`；到期或相关数据源变更会报
`CursorExpiredError`，调用方应重新搜索。过期会话通过 `maintain()` 有界清理。

指定数据库时，向 `EngineConfig(relational=RelationalConfig(...))` 传入提供方与
对应的异步连接地址。`sqlite` 使用 `sqlite+aiosqlite:///...`，`mysql` 与
`oceanbase` 使用 `mysql+aiomysql://...`，`postgres` 使用
`postgresql+asyncpg://...`；凭据由应用配置提供。

详细说明见仓库中的[架构文档](https://github.com/Zleap-AI/zleap/blob/main/packages/sag2/docs/architecture.md)、
[开发与验证](https://github.com/Zleap-AI/zleap/blob/main/packages/sag2/docs/reproduction.md)和
[接口兼容说明](https://github.com/Zleap-AI/zleap/blob/main/packages/sag2/docs/api-compatibility.md)。
这些文档也包含在源码包的 `docs/` 目录中。
