# 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 模型生成)