2643616413 преди 1 ден
родител
ревизия
666541ebde
променени са 1 файла, в които са добавени 465 реда и са изтрити 0 реда
  1. 465 0
      algo/docs/API.md

+ 465 - 0
algo/docs/API.md

@@ -0,0 +1,465 @@
+# finrep-algo-agent HTTP 接口说明
+
+本文档与当前代码实现一致,描述 **finrep-algo-agent**(FastAPI)对外暴露的全部 HTTP 接口。
+
+- **服务标题**:`finrep-algo-agent`(见 `finrep_algo_agent.main:app`)
+- **版本**:`0.1.0`(见 `finrep_algo_agent.__version__`)
+- **机器可读契约**:服务启动后访问 **`GET /openapi.json`**;交互式文档 **`GET /docs`**(Swagger UI)
+
+默认本地启动示例(端口以实际为准,README 中为 `8002`):
+
+```text
+http://127.0.0.1:8002
+```
+
+---
+
+## 1. 环境与运行模式
+
+### 1.1 环境变量前缀
+
+所有配置以 **`FINREP_`** 为前缀,可由环境变量或 **`algo/.env`** 注入(见 `config/settings.py`)。
+
+与接口行为相关的常用项:
+
+| 变量 | 说明 |
+|------|------|
+| `FINREP_STUB_SKILLS` | `true` 时:`/v1/outline/*`、`/v1/section` **不调用 LLM**,返回占位数据;`false` 时走真实模型 |
+| `FINREP_LLM_API_KEY` | 文本生成密钥;**Embedding 未单独配置时会回退使用该密钥** |
+| `FINREP_LLM_BASE_URL` / `FINREP_LLM_MODEL` / `FINREP_LLM_TIMEOUT_SECONDS` | 文本模型网关 |
+| `FINREP_EMBEDDING_API_KEY` | 向量模型密钥(可空,回退 `FINREP_LLM_API_KEY`) |
+| `FINREP_EMBEDDING_BASE_URL` / `FINREP_EMBEDDING_MODEL` / `FINREP_EMBEDDING_TIMEOUT_SECONDS` | 向量模型 |
+| `FINREP_OCR_API_KEY` | OCR 密钥(可空,回退 `FINREP_LLM_API_KEY`) |
+| `FINREP_OCR_BASE_URL` / `FINREP_OCR_MODEL` / `FINREP_OCR_TIMEOUT_SECONDS` | OCR 模型 |
+| `FINREP_RAG_CHUNK_SIZE` | RAG 分块字符数近似上限 |
+| `FINREP_RAG_CHUNK_OVERLAP` | 分块重叠字符数 |
+| `FINREP_RAG_DEFAULT_TOP_K` | 检索默认 `top_k`(请求未传 `top_k` 时使用) |
+| `FINREP_RAG_EMBEDDING_BATCH_SIZE` | 单次向量化批大小 |
+
+`FINREP_SERVICE_TOKEN` 已在配置中预留,**当前路由层未做统一鉴权校验**;若需服务间鉴权,由网关或后续中间件实现。
+
+### 1.2 RAG 存储
+
+RAG 索引使用进程内 **`InMemoryRagStore`**(按 `task_id` 隔离)。**服务重启后数据清空**;多 Worker 各进程索引不共享。
+
+---
+
+## 2. 统一约定
+
+### 2.1 Content-Type
+
+- JSON 接口:`Content-Type: application/json`
+- 文件入库:`multipart/form-data`(见 `/v1/rag/ingest-files`)
+
+### 2.2 常见 HTTP 状态码
+
+| 状态码 | 场景 |
+|--------|------|
+| 200 | 成功 |
+| 400 | 配置缺失(如未配置密钥) |
+| 422 | 请求体验证失败,或业务侧 `ValueError`(大纲/段落技能、RAG 参数等) |
+| 502 | RAG 向量化或检索过程中未捕获的下游异常,包装为业务可读 `detail` |
+
+---
+
+## 3. 接口总览
+
+| 方法 | 路径 | 标签 / 说明 |
+|------|------|----------------|
+| GET | `/health` | 健康检查 |
+| GET | `/debug/runtime` | 当前运行配置快照(非敏感) |
+| GET | `/debug/llm` | 探测 LLM 连通性 |
+| GET | `/debug/embedding` | 探测 Embedding 连通性 |
+| GET | `/debug/ocr` | 探测 OCR(需 query `image_url`) |
+| POST | `/v1/outline/l1` | 一级大纲 |
+| POST | `/v1/outline/l2` | 二级 / 末级结构(单章) |
+| POST | `/v1/section` | 单知识单元段落生成 |
+| POST | `/v1/rag/ingest-files` | 上传文件 → 解析 → 分块 → 入库 |
+| POST | `/v1/rag/ingest` | 纯文本文档入库 |
+| POST | `/v1/rag/retrieve` | 向量相似度检索 |
+| DELETE | `/v1/rag/{task_id}` | 删除某任务下 RAG 索引 |
+
+---
+
+## 4. 健康检查
+
+### `GET /health`
+
+**响应体**(`application/json`):
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `status` | string | 固定为 `ok` |
+| `service` | string | 固定为 `finrep-algo-agent` |
+| `version` | string | 包版本,如 `0.1.0` |
+
+**示例**:`{"status":"ok","service":"finrep-algo-agent","version":"0.1.0"}`
+
+---
+
+## 5. 调试接口
+
+### `GET /debug/runtime`
+
+返回当前有效配置摘要(不含 API Key)。
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `stub_skills` | boolean | 是否占位技能模式 |
+| `llm_model` | string | 文本模型名 |
+| `embedding_model` | string | 向量模型名 |
+| `ocr_model` | string | OCR 模型名 |
+| `rag_defaults` | object | `chunk_size`、`chunk_overlap`、`top_k`、`embedding_batch_size` |
+
+---
+
+### `GET /debug/llm`
+
+调用一次短对话,验证 **`FINREP_LLM_API_KEY`** 与网关可用。
+
+- **400**:`FINREP_LLM_API_KEY` 未配置
+
+**响应体**:
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `model` | string | 当前 LLM 模型名 |
+| `base_url` | string | LLM base_url |
+| `text_sample` | string | 模型回复截断至约 200 字符 |
+
+---
+
+### `GET /debug/embedding`
+
+调用一次 Embedding,验证 **`FINREP_EMBEDDING_API_KEY` 或 `FINREP_LLM_API_KEY`**。
+
+- **400**:上述两者均未配置
+
+**响应体**:
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `model` | string | 当前 embedding 模型名 |
+| `base_url` | string | embedding base_url |
+| `dim` | integer | 向量维度 |
+| `head` | array | 向量前 8 维 |
+
+---
+
+### `GET /debug/ocr`
+
+**Query 参数**(必填):
+
+| 参数 | 类型 | 说明 |
+|------|------|------|
+| `image_url` | string | 可公网或可访问的图片 URL |
+
+验证 **`FINREP_OCR_API_KEY` 或 `FINREP_LLM_API_KEY`**。
+
+- **400**:上述两者均未配置
+
+**响应体**:
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `model` | string | OCR 模型名 |
+| `base_url` | string | OCR base_url |
+| `text_sample` | string | 识别文本截断至约 400 字符 |
+
+---
+
+## 6. 一级大纲
+
+### `POST /v1/outline/l1`
+
+**Content-Type**:`application/json`
+
+**请求体**:`OutlineL1Request`
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `report_type` | string | 是 | 报告类型,如「项目融资」「资产管理」「并购重组」 |
+| `task_id` | string | 否 | 任务 ID |
+| `tenant_id` | string | 否 | 租户 ID |
+| `agreement_amount` | number / string | 否 | 协议金额(`Decimal`,JSON 中可用数字或字符串) |
+| `enterprise_type` | string | 否 | 企业类型 |
+| `group_business_segments` | string[] | 否 | 集团板块名称列表 |
+| `industry_type` | string | 否 | 行业类型 |
+| `has_independent_report` | boolean | 否 | 是否存在独立调查报告 |
+| `independent_report_types` | string[] | 否 | 独立报告类型列表 |
+| `candidate_financing_tools` | string[] | 否 | 拟分析融资工具 |
+| `recommended_financing_tools` | string[] | 否 | 拟最终推荐融资工具 |
+| `other_requirements` | string | 否 | 其他要求 |
+| `chapter_candidates` | object[] | 否 | 一级章节候选;**非空时覆盖模板内嵌候选**。每项至少含 `chapter_id`、`chapter_name`;**允许任意扩展字段**(原样进入提示词) |
+
+`chapter_candidates` 每项结构(`ChapterCandidate`):
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `chapter_id` | string | 章节 ID(须与模型输出一致,不可改写) |
+| `chapter_name` | string | 章节名称(同上) |
+| (其他键) | 任意 | 扩展属性,如重要性、适用条件等 |
+
+**响应体**:`OutlineL1Response`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `chapter_results` | array | 与候选 **数量、顺序一致**(若请求传了非空 `chapter_candidates`);每项见下表 |
+| `overall_logic` | string | 全篇结构逻辑说明 |
+
+`chapter_results` 每项(`ChapterL1Result`):
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `chapter_id` | string | 须与输入候选一致 |
+| `chapter_name` | string | 须与输入候选一致 |
+| `presentation_enum` | string | `S1` 独立呈现 / `S2` 不呈现 |
+| `paragraph_count_enum` | string | `P0`~`P4`;**`S2` 必须为 `P1`;`S1` 不可为 `P0`**(服务端校验) |
+| `reason` | string | 判断理由 |
+
+**业务校验**(非空 `chapter_candidates` 时,见 `skills/outline_l1/outline_l1.py`):
+
+- `len(chapter_results)` 必须等于 `len(chapter_candidates)`
+- 每条 `chapter_id`、`chapter_name` 必须与对应候选完全一致  
+不满足则 **422**
+
+**`FINREP_STUB_SKILLS=true`**:不请求 LLM,返回固定占位结构。
+
+---
+
+## 7. 二级大纲(单章)
+
+### `POST /v1/outline/l2`
+
+**Content-Type**:`application/json`
+
+一次请求只生成 **一个一级章** 下的末级结构;完整报告需对多个一级章分别调用并由编排方合并。
+
+**请求体**:`OutlineL2Request`
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `chapter_name` | string | 是 | 一级章名称 |
+| `chapter_no` | string | 是 | 一级章编号(展示/排序用) |
+| `l1_chapter_id` | string | 否 | L1 的 `chapter_id`,用于模板分支匹配内置末级清单 |
+| `chapter_paragraph_count_enum` | string | 否 | L1 该章 `paragraph_count_enum` |
+| `chapter_presentation_enum` | string | 否 | L1 该章 `presentation_enum`(`S1`/`S2`) |
+| `chapter_reason` | string | 否 | L1 该章 `reason` |
+| `overall_logic` | string | 否 | L1 返回的 `overall_logic` |
+| `leaf_chapter_candidates` | object[] | 否 | 末级候选覆盖列表;空则走模板内置分支 |
+| `l1_task_snapshot` | object | 否 | 与 **`OutlineL1Request` 同结构** 的快照,用于拼报告背景 |
+| `report_type` | string | 否 | 可与 `l1_task_snapshot` 二选一或同时提供 |
+| `agreement_amount` 等 | 同 L1 扁平字段 | 否 | 无 `l1_task_snapshot` 时可用于拼背景 |
+| `l1_context` | object | 否 | 额外上下文 |
+
+**响应体**:`OutlineL2Response`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `chapter_name` | string | 须与请求一致(真实模式下模型若不一致会 422) |
+| `chapter_no` | string | 须与请求一致 |
+| `chapter_structure` | array | 末级节点列表(`ChapterStructureNode`) |
+| `structure_logic` | string | 结构逻辑说明 |
+
+`ChapterStructureNode`:
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `node_id` | string | 节点 ID |
+| `node_name` | string | 节点名称 |
+| `node_no` | string | 编号 |
+| `node_level` | integer | 层级 |
+| `parent_node_id` | string / null | 父节点 ID |
+| `source_type` | string / null | 来源类型 |
+| `source_candidate_name` | string / null | 候选名 |
+| `is_selected` | boolean | 默认 `true` |
+
+**`chapter_presentation_enum = S2` 时的服务端约束**:
+
+- 解析成功后若 `chapter_structure` **非空**,抛出 `ValueError` → **422**  
+- **Stub 模式**下对 `S2` 会直接返回 **空** `chapter_structure`
+
+**`FINREP_STUB_SKILLS=true`**:不请求 LLM;`S1` 返回占位节点,`S2` 返回空结构。
+
+---
+
+## 8. 段落生成
+
+### `POST /v1/section`
+
+**Content-Type**:`application/json`
+
+**请求体**:`SectionRequest`
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `knowledge_unit_id` | string | 是 | 知识单元 ID |
+| `template_type` | string | 是 | `info` / `analysis` / `metric` / `judgment`(非法值 → 422) |
+| `task_id` | string | 否 | RAG 召回、`task_id` 隔离用 |
+| `tenant_id` | string | 否 | 租户 ID |
+| `report_type` | string | 否 | 报告类型 |
+| `paragraph_logic` | string | 否 | 撰写逻辑 |
+| `paragraph_position` | string | 否 | 段落定位 |
+| `overall_logic` | string | 否 | 全篇逻辑 |
+| `chapter_logic` | string | 否 | 章逻辑 |
+| `task_input` | object | 否 | 任务级输入 |
+| `data_package` | object | 否 | 数据包(召回结果会合并进来) |
+| `example` | string | 否 | 示例 |
+| `notes` | string | 否 | 备注 |
+| `rag_recall` | boolean | 否 | 默认 `false`;`true` 时需 `task_id` 且已配置 embedding/llm key |
+| `rag_query` | string | 否 | 召回查询;空则拼接 `paragraph_position`、`paragraph_logic`、`knowledge_unit_id` |
+| `rag_top_k` | integer | 否 | 传给 RAG;空则使用服务默认 `rag_default_top_k` |
+| `rag_min_score` | number | 否 | 相似度下限,低于则过滤 |
+
+**响应体**:`SectionResponse`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `generated_text` | string | 生成正文 |
+| `usage` | object | `TokenUsage`:`prompt_tokens`、`completion_tokens`(当前实现多为 0) |
+
+**`rag_recall=true`** 时:先 `retrieve`,将结果写入 `data_package["rag_recall"]`(含 `query`、`hits`、`formatted_context`),再渲染模板并调用 LLM。
+
+**`FINREP_STUB_SKILLS=true`**:不请求 LLM,返回占位正文。
+
+---
+
+## 9. RAG
+
+基路径:`/v1/rag`(路由前缀见 `main.py`)
+
+入库与检索均需 **`FINREP_EMBEDDING_API_KEY` 或 `FINREP_LLM_API_KEY`**;否则对应接口 **400**。
+
+### `POST /v1/rag/ingest-files`
+
+**Content-Type**:`multipart/form-data`
+
+| 表单字段 | 类型 | 必填 | 说明 |
+|----------|------|------|------|
+| `task_id` | string | 是 | 任务 ID,索引按任务隔离 |
+| `replace` | boolean | 否 | 默认 `true`;`true` 时替换该任务已有块;`false` 时在原索引上追加 |
+| `files` | file[] | 是 | 至少一个文件;支持常见文本及 PDF(见 `rag/ingestion/file_extract.py`) |
+
+**行为概要**:逐文件读 bytes → 抽取文本 → 有文本的合并为 `RagDocumentIn` → 调用与 `/ingest` 相同的分块与向量化逻辑。
+
+- **400**:未配置密钥  
+- **422**:`files` 为空;或全部文件无有效文本  
+- **502**:向量化异常  
+
+**响应体**:`RagIngestFilesResponse`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `task_id` | string | 任务 ID |
+| `document_count` | integer | 成功参与入库的文档数 |
+| `chunk_count` | integer | 写入的向量块数量 |
+| `files` | array | 每文件处理结果 `RagFileProcessResult` |
+
+`RagFileProcessResult`:
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `filename` | string | 文件名 |
+| `doc_id` | string | 派生文档 ID |
+| `characters` | integer | 抽取字符数 |
+| `skipped` | boolean | 无有效文本时为 `true` |
+| `warning` | string / null | 解析警告 |
+
+---
+
+### `POST /v1/rag/ingest`
+
+**Content-Type**:`application/json`
+
+**请求体**:`RagIngestRequest`
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `task_id` | string | 是 | 任务 ID |
+| `tenant_id` | string | 否 | 租户(预留) |
+| `documents` | array | 是 | 至少 1 条 `RagDocumentIn` |
+| `replace` | boolean | 否 | `true` 覆盖任务索引,`false` 追加 |
+
+`RagDocumentIn`:
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `doc_id` | string | 是 | 任务内文档唯一标识 |
+| `title` | string | 否 | 标题 |
+| `text` | string | 是 | 待切分全文 |
+| `source_label` | string | 否 | 来源展示名 |
+| `page_start` | integer | 否 | 起始页 |
+| `page_end` | integer | 否 | 结束页 |
+
+若所有文档切分后无块且 `replace=true`,仍会清空该任务索引并返回 `chunk_count=0`(见 `RagService.ingest`)。
+
+**响应体**:`RagIngestResponse`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `task_id` | string | 任务 ID |
+| `document_count` | integer | 文档条数 |
+| `chunk_count` | integer | 块数量 |
+
+---
+
+### `POST /v1/rag/retrieve`
+
+**Content-Type**:`application/json`
+
+**请求体**:`RagRetrieveRequest`
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `task_id` | string | 是 | 任务 ID |
+| `tenant_id` | string | 否 | 租户(预留) |
+| `query` | string | 是 | 查询句,**最小长度 1** |
+| `top_k` | integer | 否 | 返回条数上限;**未传时使用 `FINREP_RAG_DEFAULT_TOP_K`**,且服务端会将 `k` 约束为 **≥1** |
+| `min_score` | number | 否 | 最小相似度;未传则不过滤 |
+
+**响应体**:`RagRetrieveResponse`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `hits` | array | `RagHit` 列表 |
+| `formatted_context` | string | 拼接后的引用上下文文本 |
+
+`RagHit`:
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `chunk_id` | string | 块 ID |
+| `text` | string | 块文本 |
+| `score` | number | 余弦相似度 |
+| `doc_id` | string | 文档 ID |
+| `title` | string | 标题 |
+| `source_label` | string | 来源 |
+| `chunk_index` | integer | 块序号 |
+| `page_start` / `page_end` | integer / null | 页码 |
+| `extra` | object | 扩展 |
+
+任务无索引时返回 **空** `hits` 与空的 `formatted_context`(不报错)。
+
+**502**:检索或 embedding 异常。
+
+---
+
+### `DELETE /v1/rag/{task_id}`
+
+**路径参数**:`task_id` — 要删除索引的任务 ID。
+
+**响应体**:`RagDeleteResponse`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `task_id` | string | 与路径一致 |
+| `deleted` | boolean | 删除前该任务**是否已有块**;无数据时为 `false` |
+
+---
+
+## 10. 文档维护说明
+
+若接口或 Schema 发生变更,请同步更新:
+
+- 本文档:`algo/docs/API.md`
+- 运行时契约:以 **`/openapi.json`** 为准(由 FastAPI 自 Pydantic 模型生成)