2643616413 2 dní pred
commit
e57abd8145
100 zmenil súbory, kde vykonal 1598 pridanie a 0 odobranie
  1. 189 0
      STRUCTURE.md
  2. 35 0
      algo/README.md
  3. 0 0
      algo/deploy/.gitkeep
  4. 16 0
      algo/deploy/Dockerfile
  5. 0 0
      algo/docs/.gitkeep
  6. 11 0
      algo/env.example
  7. 31 0
      algo/pyproject.toml
  8. 0 0
      algo/scripts/.gitkeep
  9. 43 0
      algo/src/finrep_algo_agent.egg-info/PKG-INFO
  10. 36 0
      algo/src/finrep_algo_agent.egg-info/SOURCES.txt
  11. 1 0
      algo/src/finrep_algo_agent.egg-info/dependency_links.txt
  12. 10 0
      algo/src/finrep_algo_agent.egg-info/requires.txt
  13. 1 0
      algo/src/finrep_algo_agent.egg-info/top_level.txt
  14. 3 0
      algo/src/finrep_algo_agent/__init__.py
  15. BIN
      algo/src/finrep_algo_agent/__pycache__/__init__.cpython-312.pyc
  16. BIN
      algo/src/finrep_algo_agent/__pycache__/main.cpython-312.pyc
  17. 0 0
      algo/src/finrep_algo_agent/agent/.gitkeep
  18. 1 0
      algo/src/finrep_algo_agent/api/__init__.py
  19. BIN
      algo/src/finrep_algo_agent/api/__pycache__/__init__.cpython-312.pyc
  20. BIN
      algo/src/finrep_algo_agent/api/__pycache__/deps.cpython-312.pyc
  21. 16 0
      algo/src/finrep_algo_agent/api/deps.py
  22. 0 0
      algo/src/finrep_algo_agent/api/deps/.gitkeep
  23. 0 0
      algo/src/finrep_algo_agent/api/routers/.gitkeep
  24. 5 0
      algo/src/finrep_algo_agent/api/routers/__init__.py
  25. BIN
      algo/src/finrep_algo_agent/api/routers/__pycache__/__init__.cpython-312.pyc
  26. BIN
      algo/src/finrep_algo_agent/api/routers/__pycache__/health.cpython-312.pyc
  27. BIN
      algo/src/finrep_algo_agent/api/routers/__pycache__/outline.cpython-312.pyc
  28. BIN
      algo/src/finrep_algo_agent/api/routers/__pycache__/section.cpython-312.pyc
  29. 12 0
      algo/src/finrep_algo_agent/api/routers/health.py
  30. 25 0
      algo/src/finrep_algo_agent/api/routers/outline.py
  31. 17 0
      algo/src/finrep_algo_agent/api/routers/section.py
  32. 0 0
      algo/src/finrep_algo_agent/config/.gitkeep
  33. 3 0
      algo/src/finrep_algo_agent/config/__init__.py
  34. BIN
      algo/src/finrep_algo_agent/config/__pycache__/__init__.cpython-312.pyc
  35. BIN
      algo/src/finrep_algo_agent/config/__pycache__/settings.cpython-312.pyc
  36. 27 0
      algo/src/finrep_algo_agent/config/settings.py
  37. 0 0
      algo/src/finrep_algo_agent/core/.gitkeep
  38. 3 0
      algo/src/finrep_algo_agent/llm/__init__.py
  39. BIN
      algo/src/finrep_algo_agent/llm/__pycache__/__init__.cpython-312.pyc
  40. BIN
      algo/src/finrep_algo_agent/llm/__pycache__/client.cpython-312.pyc
  41. 68 0
      algo/src/finrep_algo_agent/llm/client.py
  42. 0 0
      algo/src/finrep_algo_agent/llm/client/.gitkeep
  43. 0 0
      algo/src/finrep_algo_agent/llm/providers/.gitkeep
  44. 0 0
      algo/src/finrep_algo_agent/llm/router/.gitkeep
  45. 22 0
      algo/src/finrep_algo_agent/main.py
  46. 0 0
      algo/src/finrep_algo_agent/observability/.gitkeep
  47. 11 0
      algo/src/finrep_algo_agent/prompts/__init__.py
  48. BIN
      algo/src/finrep_algo_agent/prompts/__pycache__/__init__.cpython-312.pyc
  49. BIN
      algo/src/finrep_algo_agent/prompts/__pycache__/builders.cpython-312.pyc
  50. 142 0
      algo/src/finrep_algo_agent/prompts/builders.py
  51. 0 0
      algo/src/finrep_algo_agent/prompts/builders/.gitkeep
  52. 0 0
      algo/src/finrep_algo_agent/prompts/templates/.gitkeep
  53. 106 0
      algo/src/finrep_algo_agent/prompts/templates/outline_l1.j2
  54. 137 0
      algo/src/finrep_algo_agent/prompts/templates/outline_l2.j2
  55. 52 0
      algo/src/finrep_algo_agent/prompts/templates/section_analysis.j2
  56. 51 0
      algo/src/finrep_algo_agent/prompts/templates/section_info.j2
  57. 53 0
      algo/src/finrep_algo_agent/prompts/templates/section_judgment.j2
  58. 53 0
      algo/src/finrep_algo_agent/prompts/templates/section_metric.j2
  59. 0 0
      algo/src/finrep_algo_agent/rag/ingestion/.gitkeep
  60. 0 0
      algo/src/finrep_algo_agent/rag/retrieval/.gitkeep
  61. 0 0
      algo/src/finrep_algo_agent/rag/vectorstore/.gitkeep
  62. 0 0
      algo/src/finrep_algo_agent/schemas/.gitkeep
  63. 23 0
      algo/src/finrep_algo_agent/schemas/__init__.py
  64. BIN
      algo/src/finrep_algo_agent/schemas/__pycache__/__init__.cpython-312.pyc
  65. BIN
      algo/src/finrep_algo_agent/schemas/__pycache__/outline.cpython-312.pyc
  66. BIN
      algo/src/finrep_algo_agent/schemas/__pycache__/section.cpython-312.pyc
  67. 89 0
      algo/src/finrep_algo_agent/schemas/outline.py
  68. 31 0
      algo/src/finrep_algo_agent/schemas/section.py
  69. 0 0
      algo/src/finrep_algo_agent/services/.gitkeep
  70. 5 0
      algo/src/finrep_algo_agent/skills/__init__.py
  71. BIN
      algo/src/finrep_algo_agent/skills/__pycache__/__init__.cpython-312.pyc
  72. BIN
      algo/src/finrep_algo_agent/skills/__pycache__/outline_l1.cpython-312.pyc
  73. BIN
      algo/src/finrep_algo_agent/skills/__pycache__/outline_l2.cpython-312.pyc
  74. BIN
      algo/src/finrep_algo_agent/skills/__pycache__/section_gen.cpython-312.pyc
  75. 75 0
      algo/src/finrep_algo_agent/skills/outline_l1.py
  76. 0 0
      algo/src/finrep_algo_agent/skills/outline_l1/.gitkeep
  77. 65 0
      algo/src/finrep_algo_agent/skills/outline_l2.py
  78. 0 0
      algo/src/finrep_algo_agent/skills/outline_l2/.gitkeep
  79. 0 0
      algo/src/finrep_algo_agent/skills/rag_retrieve/.gitkeep
  80. 39 0
      algo/src/finrep_algo_agent/skills/section_gen.py
  81. 0 0
      algo/src/finrep_algo_agent/skills/section_gen/.gitkeep
  82. 0 0
      algo/tests/__init__.py
  83. BIN
      algo/tests/__pycache__/__init__.cpython-312.pyc
  84. BIN
      algo/tests/__pycache__/test_health.cpython-312-pytest-9.0.2.pyc
  85. BIN
      algo/tests/__pycache__/test_prompts.cpython-312-pytest-9.0.2.pyc
  86. 0 0
      algo/tests/integration/.gitkeep
  87. 32 0
      algo/tests/test_health.py
  88. 59 0
      algo/tests/test_prompts.py
  89. 0 0
      algo/tests/unit/.gitkeep
  90. 0 0
      backend/docs/.gitkeep
  91. 0 0
      backend/finrep-application/src/main/java/com/yuxin/finrep/application/command/.gitkeep
  92. 0 0
      backend/finrep-application/src/main/java/com/yuxin/finrep/application/orchestration/.gitkeep
  93. 0 0
      backend/finrep-application/src/main/java/com/yuxin/finrep/application/policy/.gitkeep
  94. 0 0
      backend/finrep-application/src/main/java/com/yuxin/finrep/application/port/in/.gitkeep
  95. 0 0
      backend/finrep-application/src/main/java/com/yuxin/finrep/application/port/out/.gitkeep
  96. 0 0
      backend/finrep-application/src/main/java/com/yuxin/finrep/application/query/.gitkeep
  97. 0 0
      backend/finrep-application/src/main/java/com/yuxin/finrep/application/service/.gitkeep
  98. 0 0
      backend/finrep-application/src/main/resources/.gitkeep
  99. 0 0
      backend/finrep-application/src/test/java/com/yuxin/finrep/application/.gitkeep
  100. 0 0
      backend/finrep-common/src/main/java/com/yuxin/finrep/common/constant/.gitkeep

+ 189 - 0
STRUCTURE.md

@@ -0,0 +1,189 @@
+# finrep-report 工程目录说明
+
+本仓库为 **单仓(Monorepo)**,聚合 **Web 前端**、**Java 业务后端**、**Python 算法 / Agent** 三端。前端 **只调用 Java 后端**;Java 通过 **pythonagent** 适配层调用 **algo** 服务。
+
+**约定**:Java 根包名为 `com.yuxin.finrep`;Python 包根为 `finrep_algo_agent`。目录中当前多为占位(`.gitkeep`),后续可在此结构上添加 `package.json`、`pom.xml`、`pyproject.toml` 等。
+
+---
+
+## 一、`frontend/`(Web 前端)
+
+面向业务人员的 **浏览器 SPA**(建议技术栈:React + 企业级组件库)。**不直接请求算法端**;统一走 `backend` 提供的 HTTPS API 与 SSE。
+
+
+| 子目录                   | 作用                                                                                          |
+| --------------------- | ------------------------------------------------------------------------------------------- |
+| `**public/`**         | 不参与打包的静态资源:如 `favicon.ico`、`robots.txt`、固定可缓存的公共文件等。                                        |
+| `**src/assets/**`     | 需经构建处理的资源:图片、图标、小号字体等(由打包工具优化路径与哈希)。                                                        |
+| `**src/components/**` | 可复用 UI 组件;其中 `**common/**` 放置与设计系统无关的通用控件(按钮封装、空状态、加载中等)。业务强相关大块可再建 `features/` 等子目录(按需扩展)。 |
+| `**src/layouts/**`    | 整体布局壳:顶栏、侧栏、内容区组合;各业务页挂载在布局之下。                                                              |
+| `**src/pages/**`      | **页面级**入口:与路由一一对应,例如任务列表、任务向导、大纲确认树、数据确认、报告预览与导出等。                                          |
+| `**src/routes/`**     | 路由表、路由守卫(登录态、权限)、懒加载页面配置。                                                                   |
+| `**src/services/**`   | **HTTP / SSE 客户端封装**:调用 Java 的 REST、订阅任务进度;集中处理 baseURL、Token、错误码与 TraceId 头(若规范要求)。        |
+| `**src/hooks/`**      | React Hooks:如任务轮询、SSE 连接、表单与向导步骤状态等与 UI 紧耦合的复用逻辑。                                           |
+| `**src/stores/**`     | 全局或跨页状态(Redux、Zustand、Recoil 等选型待定):当前任务、用户信息、租户上下文等。                                       |
+| `**src/utils/**`      | 纯函数工具:日期格式化、树数据转换、与后端 DTO 对齐的轻量映射等。                                                         |
+| `**src/styles/**`     | 全局样式、CSS 变量、主题 token;与组件库主题对齐。                                                              |
+| `**src/types/**`      | TypeScript 类型:与后端 OpenAPI 生成或手写对齐的请求 / 响应、枚举、领域 DTO。                                        |
+| `**tests/**`          | 前端单测 / 组件测(如 Vitest + Testing Library);与 `src` 目录结构可镜像或按需组织。                                |
+
+
+---
+
+## 二、`backend/`(Java 业务后端)
+
+**Spring Boot 多模块** 结构:承载 **鉴权、多租户、任务状态机、落库、行内接口、OSS、导出**,以及 **调用算法服务的 HTTP/gRPC 客户端**。
+
+### 2.1 模块总览
+
+
+| 模块                          | 作用                                                                                |
+| --------------------------- | --------------------------------------------------------------------------------- |
+| `**finrep-parent`**         | 父 POM(后续添加):统一依赖版本、插件、子模块列表。                                                      |
+| `**finrep-common**`         | 跨模块通用代码:上下文、异常、工具、常量。                                                             |
+| `**finrep-domain**`         | 领域模型与领域语义分包(任务、大纲、数据准备、生成等),**不依赖** Web 与具体持久化框架(或仅依赖极少注解,按团队规约)。                 |
+| `**finrep-application`**    | 应用服务层:**编排 / 状态机推进**、用例(command/query)、**入站/出站端口接口**、面向产品的策略(如选默认模型策略的 Java 侧部分)。 |
+| `**finrep-infrastructure`** | **落地实现**:数据库、Redis、MQ、对象存储、外部 HTTP、**pythonagent**、可观测性配置等。                       |
+| `**finrep-web`**            | **对外 HTTP/SSE**:REST 控制器、请求/响应 DTO、安全、全局异常、WebMvc 配置。                             |
+| `**finrep-worker`**         | **异步消费者**(与 MQ 配合):长耗时阶段消费、分布式锁、可选定时补偿。                                           |
+| `**finrep-contract`**       | **OpenAPI 等契约**:`openapi/` 下放 YAML/JSON,可供前后端与 Python 对齐(与 Java 代码生成配置配合使用)。      |
+| `**docs/`**                 | 后端专项说明(部署参数、环境约定等),与仓库根目录产品文档区分。                                                  |
+
+
+### 2.2 `finrep-common` 包路径 `.../common/`
+
+
+| 包               | 作用                                                                  |
+| --------------- | ------------------------------------------------------------------- |
+| `**context**`   | `TenantContext`、`UserContext`、traceId 等在请求链路中的承载与 ThreadLocal/传递封装。 |
+| `**exception**` | 业务异常基类、错误码枚举;与 `web` 层统一异常 advice 对应。                               |
+| `**util**`      | 通用工具类(字符串、集合、JSON 辅助等)。                                             |
+| `**constant**`  | 全局常量、HTTP 头名称约定、状态枚举与持久化一致时集中定义(或与 domain 协同)。                      |
+
+
+### 2.3 `finrep-domain` 包路径 `.../domain/`
+
+
+| 包                | 作用                                       |
+| ---------------- | ---------------------------------------- |
+| `**task**`       | 报告任务聚合、任务生命周期、状态机枚举(领域语义层面)。             |
+| `**outline**`    | 一级/二级大纲、最终知识单元清单等领域对象与规则(不含 HTTP 与 ORM)。 |
+| `**dataprep**`   | 知识单元数据包、数据确认等领域概念。                       |
+| `**generation**` | 单段生成结果、完整报告等领域概念。                        |
+| `**knowledge**`  | 与「知识单元 / 适用范围」相关的领域类型(若与预置配置边界相关)。       |
+| `**tenant**`     | 租户/组织值对象、隔离相关领域规则。                       |
+| `**shared**`     | 域内多处共用的值对象、标识符类型等。                       |
+
+
+### 2.4 `finrep-application` 包路径 `.../application/`
+
+
+| 包                   | 作用                                                           |
+| ------------------- | ------------------------------------------------------------ |
+| `**orchestration**` | **主链路编排**:阶段推进、与人工卡点协作、调用出站端口(持久化、MQ、算法客户端)。                 |
+| `**command`**       | 写用例:创建任务、提交大纲确认、提交数据确认、触发生成等。                                |
+| `**query**`         | 读用例:任务详情、大纲草稿、数据包展示、报告预览数据等。                                 |
+| `**service**`       | 复杂用例实现类、跨聚合协调(若与 command/query 拆分并行存在)。                      |
+| `**policy**`        | 应用层策略:例如 Java 侧路由到不同算法接口、租户级功能开关等。                           |
+| `**port/in**`       | **入站端口**:由 `web` 或 `worker` 调用的应用服务接口。                       |
+| `**port/out`**      | **出站端口**:持久化、缓存、消息、**算法服务**、OSS 等由 `infrastructure` 实现的接口定义。 |
+
+
+### 2.5 `finrep-infrastructure` 包路径 `.../infrastructure/`
+
+
+| 包                             | 作用                                                                                      |
+| ----------------------------- | --------------------------------------------------------------------------------------- |
+| `**persistence/entity`**      | JPA Entity / 或 MyBatis PO 等,与表结构对应。                                                     |
+| `**persistence/repository**`  | 仓储实现:封装 SQL / JPA 访问。                                                                   |
+| `**persistence/mapper**`      | MyBatis Mapper 接口与 XML(若选用 MyBatis)。                                                    |
+| `**redis**`                   | 分布式锁、缓存、限流计数、任务进度缓存等。                                                                   |
+| `**mq/producer**`             | 投递异步任务消息。                                                                               |
+| `**mq/consumer**`             | 若部分消费逻辑共享;**也可**仅放在 `finrep-worker` 由团队二选一。                                             |
+| `**mq/message`**              | 消息体 DTO、Topic/Tag 常量。                                                                   |
+| `**integration/http**`        | 调用**行内** HTTP API(工商、风险、通用网关等)。                                                         |
+| `**integration/metrics`**     | **指标平台**或统一数据服务客户端。                                                                     |
+| `**integration/external`**    | 其他外部系统适配(与 metrics/http 并列归类)。                                                          |
+| `**integration/pythonagent**` | **调用 `algo`(Python)** 的客户端:OpenAPI 生成或手写 WebClient/Feign;统一超时、重试、mTLS/Token、TraceId 传递。 |
+| `**storage/oss`**             | 对象存储上传/下载/预签名 URL。                                                                      |
+| `**config**`                  | Bean 装配、多数据源、RestTemplate/WebClient 工厂等。                                                |
+| `**observability**`           | 日志 MDC、`MeterRegistry`、Tracing 与 Java 侧埋点。                                              |
+| `**resources/db/migration**`  | Flyway / Liquibase 脚本目录。                                                                |
+
+
+### 2.6 `finrep-web` 包路径 `.../web/`
+
+
+| 包                 | 作用                                           |
+| ----------------- | -------------------------------------------- |
+| `**rest/dto**`    | 入参/出参对象;与 OpenAPI 一致。                        |
+| `**rest/advice**` | 全局异常、统一响应包装、参数校验错误映射。                        |
+| `**sse**`         | **Server-Sent Events**:向浏览器推送任务阶段、当前知识单元进度等。 |
+| `**security`**    | Spring Security:认证、鉴权、从令牌解析租户/用户。            |
+| `**config**`      | Web 层专属配置:CORS、国际化、Jackson 等。                |
+
+
+`**resources/static` / `templates**`:若需服务端薄模板或静态页(多数 SPA 仅 `static` 放空由 Nginx 托管前端构建产物)。
+
+### 2.7 `finrep-worker` 包路径 `.../worker/`
+
+
+| 包               | 作用                                      |
+| --------------- | --------------------------------------- |
+| `**consumer**`  | MQ 监听入口:收到消息后调用 `application` 层推进或调用算法。 |
+| `**handler**`   | 按消息类型拆分的处理逻辑(大纲生成、全文生成批次等)。             |
+| `**scheduler**` | 可选 `@Scheduled`:重试、超时扫描、清理临时数据。         |
+| `**config**`    | Worker 进程专属配置(线程池、并发度等)。                |
+
+
+---
+
+## 三、`algo/`(Python 算法 / Agent 服务)
+
+**FastAPI(建议)** 对内提供 **OpenAI 兼容** 大模型调用与 **报告专用技能**:一级/二级大纲、段落生成、RAG 检索等。由 `**backend`** 内网调用;**不对公网暴露**(或与网关结合白名单)。
+
+
+| 子目录                                             | 作用                                                                             |
+| ----------------------------------------------- | ------------------------------------------------------------------------------ |
+| `**src/finrep_algo_agent/api/routers`**         | HTTP 路由:按技能或资源划分 `POST /v1/outline/l1` 等接口定义。                                  |
+| `**src/finrep_algo_agent/api/deps**`            | FastAPI `Depends`:鉴权、限流、公共请求体解析、从 Header 注入 trace/tenant。                      |
+| `**src/finrep_algo_agent/agent**`               | 多步编排入口:LangGraph 或状态图定义、Human-in-the-loop **占位**(实际卡点仍在 Java/库表时,此处仅接收已确认上下文)。 |
+| `**src/finrep_algo_agent/skills/outline_l1`**   | **一级大纲**技能:拼装 Prompt、调模型、解析/校验 JSON、返回结构化一级章节结果。                               |
+| `**src/finrep_algo_agent/skills/outline_l2`**   | **二级大纲 / 知识单元装配**技能:按章调用、章节编号结构、与需求文档 JSON 契约对齐。                               |
+| `**src/finrep_algo_agent/skills/section_gen`**  | **按知识单元生成正文**技能:模板变量注入、段落级调用与拼接顺序由 Java 调多次或一次批处理(按设计定)。                       |
+| `**src/finrep_algo_agent/skills/rag_retrieve`** | **仅检索**:返回引用片段供上游拼 Prompt;或由 `services` 组合进 `section_gen`。                     |
+| `**src/finrep_algo_agent/llm/client`**          | OpenAI 兼容 HTTP 客户端封装(base_url、api_key、超时)。                                     |
+| `**src/finrep_algo_agent/llm/router**`          | **多模型路由**:按请求元数据选择模型别名、主备降级。                                                   |
+| `**src/finrep_algo_agent/llm/providers`**       | 各厂商/集群差异适配(若路由不足以覆盖)。                                                          |
+| `**src/finrep_algo_agent/rag/ingestion**`       | 文档解析、分块、写入向量库(离线或准实时任务)。                                                       |
+| `**src/finrep_algo_agent/rag/retrieval**`       | 查询改写、相似度检索、重排序(若需要)。                                                           |
+| `**src/finrep_algo_agent/rag/vectorstore**`     | 向量库访问封装(pgvector、Milvus 等)。                                                    |
+| `**src/finrep_algo_agent/prompts/templates**`   | Jinja2 / 文本模板文件;与需求文档章节结构对齐的版本管理。                                              |
+| `**src/finrep_algo_agent/prompts/builders**`    | 从领域 DTO 生成 messages 列表的构建器代码。                                                  |
+| `**src/finrep_algo_agent/schemas**`             | Pydantic 模型:请求/响应、大纲 JSON、与 Java DTO 命名对齐说明。                                   |
+| `**src/finrep_algo_agent/services**`            | 组合技能与仓储的高层服务;供 `routers` 薄层调用。                                                 |
+| `**src/finrep_algo_agent/core**`                | 领域无关内核:常量、共用异常、运行模式枚举等。                                                        |
+| `**src/finrep_algo_agent/config**`              | `pydantic-settings` / 环境变量:模型 URL、密钥(来自密钥管理)、功能开关。                             |
+| `**src/finrep_algo_agent/observability**`       | 日志结构化、Metrics、与 OpenTelemetry 对接。                                              |
+| `**tests/unit**`                                | 单测:解析、Prompt 构建、mock LLM。                                                      |
+| `**tests/integration**`                         | 集成测:需真实或 testcontainers 模型/向量库时启用。                                             |
+| `**scripts/**`                                  | 一次性脚本:索引重建、批量评测、Prompt 回归等。                                                    |
+| `**deploy/**`                                   | Dockerfile、k8s YAML、行内发布说明。                                                    |
+| `**docs/**`                                     | 算法侧设计:接口契约、与 Java 对齐字段、模型版本记录。                                                 |
+
+
+---
+
+## 四、三端协作关系(回顾)
+
+```text
+浏览器 → frontend → backend(REST + SSE)
+                    → backend.integration.pythonagent → algo(REST)
+```
+
+任务与人工确认的 **权威状态** 在 **backend 数据库**;`algo` 尽量 **无状态或可重建**,输出结构化结果供 Java 持久化。
+
+---
+
+
+

+ 35 - 0
algo/README.md

@@ -0,0 +1,35 @@
+# finrep-algo-agent
+
+Python 算法服务骨架:健康检查、`/v1/outline/l1|l2`、`/v1/section` 占位实现,便于与 Java `pythonagent` 联调。
+
+## 本地运行
+
+```bash
+cd algo
+python -m venv .venv
+.\.venv\Scripts\activate
+pip install -e ".[dev]"
+uvicorn finrep_algo_agent.main:app --reload --host 0.0.0.0 --port 8001 --app-dir src
+```
+
+## 环境变量
+
+复制 `env.example` 为 `.env`(可选)。主要项:
+
+- `FINREP_LLM_BASE_URL`:OpenAI 兼容网关地址,如 `https://api.openai.com/v1`
+- `FINREP_LLM_API_KEY`:密钥
+- `FINREP_LLM_MODEL`:模型名
+- `FINREP_STUB_SKILLS=true`:为 `true` 时 L1/L2/Section 返回固定占位数据,不调模型
+
+## 联调
+
+- `GET http://localhost:8001/health`
+- `POST http://localhost:8001/v1/outline/l1`(JSON 见 `schemas`)
+
+## 与《MVP 需求文档》对齐的交互字段
+
+- **一级大纲 `POST /v1/outline/l1`**:请求体与需求文档「报告背景信息」「一级章节候选清单」一致:`report_type`、`agreement_amount`、`enterprise_type`、`group_business_segments`、`industry_type`、`has_independent_report`、`independent_report_types`、`candidate_financing_tools`、`recommended_financing_tools`、`other_requirements`、`chapter_candidates`(每项可含除 `chapter_id`/`chapter_name` 外的扩展字段,如重要性、适用条件,将原样写入清单文本供模型使用)。
+- **二级大纲 `POST /v1/outline/l2`**:除 `chapter_name`、`chapter_no`、`chapter_paragraph_count_enum`、`leaf_chapter_candidates` 外,需传入与需求文档「已确定的上游约束」「整体写作逻辑说明」对齐的 **`chapter_reason`**(一级该章 `reason`)、**`overall_logic`**(一级 `overall_logic`)。**报告背景**推荐整块传入 **`l1_task_snapshot`**(与 L1 请求同结构的 `OutlineL1Request`);若不传,则使用本请求上的扁平字段(`report_type`、`agreement_amount` 等)拼背景。
+- **段落生成 `POST /v1/section`**:`template_type` 取值 `info` / `analysis` / `metric` / `judgment` 对应需求文档四类模板;`overall_logic`、`chapter_logic`、`paragraph_position`、`task_input`、`data_package`、`paragraph_logic` 对应模板中「橙/蓝」占位;`example`、`notes` 对应「示例」「其他注意事项」。
+
+提示词正文位于 `src/finrep_algo_agent/prompts/templates/*.j2`,与需求文档摘录保持一致,后续仅以改模板版本迭代。

+ 0 - 0
algo/deploy/.gitkeep


+ 16 - 0
algo/deploy/Dockerfile

@@ -0,0 +1,16 @@
+# 构建:在 algo 目录执行
+#   docker build -f deploy/Dockerfile .
+
+FROM python:3.12-slim
+
+WORKDIR /app
+ENV PYTHONUNBUFFERED=1 \
+    PYTHONDONTWRITEBYTECODE=1 \
+    PIP_NO_CACHE_DIR=1
+
+COPY pyproject.toml README.md ./
+COPY src ./src
+RUN pip install .
+
+EXPOSE 8001
+CMD ["uvicorn", "finrep_algo_agent.main:app", "--host", "0.0.0.0", "--port", "8001", "--app-dir", "src"]

+ 0 - 0
algo/docs/.gitkeep


+ 11 - 0
algo/env.example

@@ -0,0 +1,11 @@
+# OpenAI 兼容接口
+FINREP_LLM_BASE_URL=https://api.openai.com/v1
+FINREP_LLM_API_KEY=
+FINREP_LLM_MODEL=gpt-4o-mini
+FINREP_LLM_TIMEOUT_SECONDS=120
+
+# true:L1/L2/Section 不调模型,返回占位 JSON(联调 Java 时可先开)
+FINREP_STUB_SKILLS=true
+
+# 可选:服务间校验(占位,后续与 Java 对齐)
+FINREP_SERVICE_TOKEN=

+ 31 - 0
algo/pyproject.toml

@@ -0,0 +1,31 @@
+[build-system]
+requires = ["setuptools>=61", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "finrep-algo-agent"
+version = "0.1.0"
+description = "财顾报告 Agent — Python 算法服务(FastAPI)"
+readme = "README.md"
+requires-python = ">=3.11"
+dependencies = [
+    "fastapi>=0.110",
+    "uvicorn[standard]>=0.27",
+    "pydantic>=2",
+    "pydantic-settings>=2",
+    "httpx>=0.27",
+    "jinja2>=3.1",
+]
+
+[project.optional-dependencies]
+dev = ["pytest>=8", "pytest-asyncio>=0.24"]
+
+[tool.setuptools.packages.find]
+where = ["src"]
+
+[tool.setuptools.package-data]
+finrep_algo_agent = ["prompts/templates/*.j2"]
+
+[tool.pytest.ini_options]
+asyncio_mode = "auto"
+testpaths = ["tests"]

+ 0 - 0
algo/scripts/.gitkeep


+ 43 - 0
algo/src/finrep_algo_agent.egg-info/PKG-INFO

@@ -0,0 +1,43 @@
+Metadata-Version: 2.4
+Name: finrep-algo-agent
+Version: 0.1.0
+Summary: 财顾报告 Agent — Python 算法服务(FastAPI)
+Requires-Python: >=3.11
+Description-Content-Type: text/markdown
+Requires-Dist: fastapi>=0.110
+Requires-Dist: uvicorn[standard]>=0.27
+Requires-Dist: pydantic>=2
+Requires-Dist: pydantic-settings>=2
+Requires-Dist: httpx>=0.27
+Requires-Dist: jinja2>=3.1
+Provides-Extra: dev
+Requires-Dist: pytest>=8; extra == "dev"
+Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
+
+# finrep-algo-agent
+
+Python 算法服务骨架:健康检查、`/v1/outline/l1|l2`、`/v1/section` 占位实现,便于与 Java `pythonagent` 联调。
+
+## 本地运行
+
+```bash
+cd algo
+python -m venv .venv
+.\.venv\Scripts\activate
+pip install -e ".[dev]"
+uvicorn finrep_algo_agent.main:app --reload --host 0.0.0.0 --port 8001 --app-dir src
+```
+
+## 环境变量
+
+复制 `env.example` 为 `.env`(可选)。主要项:
+
+- `FINREP_LLM_BASE_URL`:OpenAI 兼容网关地址,如 `https://api.openai.com/v1`
+- `FINREP_LLM_API_KEY`:密钥
+- `FINREP_LLM_MODEL`:模型名
+- `FINREP_STUB_SKILLS=true`:为 `true` 时 L1/L2/Section 返回固定占位数据,不调模型
+
+## 联调
+
+- `GET http://localhost:8001/health`
+- `POST http://localhost:8001/v1/outline/l1`(JSON 见 `schemas`)

+ 36 - 0
algo/src/finrep_algo_agent.egg-info/SOURCES.txt

@@ -0,0 +1,36 @@
+README.md
+pyproject.toml
+src/finrep_algo_agent/__init__.py
+src/finrep_algo_agent/main.py
+src/finrep_algo_agent.egg-info/PKG-INFO
+src/finrep_algo_agent.egg-info/SOURCES.txt
+src/finrep_algo_agent.egg-info/dependency_links.txt
+src/finrep_algo_agent.egg-info/requires.txt
+src/finrep_algo_agent.egg-info/top_level.txt
+src/finrep_algo_agent/api/__init__.py
+src/finrep_algo_agent/api/deps.py
+src/finrep_algo_agent/api/routers/__init__.py
+src/finrep_algo_agent/api/routers/health.py
+src/finrep_algo_agent/api/routers/outline.py
+src/finrep_algo_agent/api/routers/section.py
+src/finrep_algo_agent/config/__init__.py
+src/finrep_algo_agent/config/settings.py
+src/finrep_algo_agent/llm/__init__.py
+src/finrep_algo_agent/llm/client.py
+src/finrep_algo_agent/prompts/__init__.py
+src/finrep_algo_agent/prompts/builders.py
+src/finrep_algo_agent/prompts/templates/outline_l1.j2
+src/finrep_algo_agent/prompts/templates/outline_l2.j2
+src/finrep_algo_agent/prompts/templates/section_analysis.j2
+src/finrep_algo_agent/prompts/templates/section_info.j2
+src/finrep_algo_agent/prompts/templates/section_judgment.j2
+src/finrep_algo_agent/prompts/templates/section_metric.j2
+src/finrep_algo_agent/schemas/__init__.py
+src/finrep_algo_agent/schemas/outline.py
+src/finrep_algo_agent/schemas/section.py
+src/finrep_algo_agent/skills/__init__.py
+src/finrep_algo_agent/skills/outline_l1.py
+src/finrep_algo_agent/skills/outline_l2.py
+src/finrep_algo_agent/skills/section_gen.py
+tests/test_health.py
+tests/test_prompts.py

+ 1 - 0
algo/src/finrep_algo_agent.egg-info/dependency_links.txt

@@ -0,0 +1 @@
+

+ 10 - 0
algo/src/finrep_algo_agent.egg-info/requires.txt

@@ -0,0 +1,10 @@
+fastapi>=0.110
+uvicorn[standard]>=0.27
+pydantic>=2
+pydantic-settings>=2
+httpx>=0.27
+jinja2>=3.1
+
+[dev]
+pytest>=8
+pytest-asyncio>=0.24

+ 1 - 0
algo/src/finrep_algo_agent.egg-info/top_level.txt

@@ -0,0 +1 @@
+finrep_algo_agent

+ 3 - 0
algo/src/finrep_algo_agent/__init__.py

@@ -0,0 +1,3 @@
+"""财顾报告 Agent — Python 算法服务。"""
+
+__version__ = "0.1.0"

BIN
algo/src/finrep_algo_agent/__pycache__/__init__.cpython-312.pyc


BIN
algo/src/finrep_algo_agent/__pycache__/main.cpython-312.pyc


+ 0 - 0
algo/src/finrep_algo_agent/agent/.gitkeep


+ 1 - 0
algo/src/finrep_algo_agent/api/__init__.py

@@ -0,0 +1 @@
+"""HTTP 层:路由与依赖。"""

BIN
algo/src/finrep_algo_agent/api/__pycache__/__init__.cpython-312.pyc


BIN
algo/src/finrep_algo_agent/api/__pycache__/deps.cpython-312.pyc


+ 16 - 0
algo/src/finrep_algo_agent/api/deps.py

@@ -0,0 +1,16 @@
+from __future__ import annotations
+
+from typing import Annotated
+
+from fastapi import Depends
+
+from finrep_algo_agent.config import Settings, get_settings
+from finrep_algo_agent.llm import LlmClient
+
+
+def get_llm(settings: Annotated[Settings, Depends(get_settings)]) -> LlmClient:
+    return LlmClient(settings)
+
+
+SettingsDep = Annotated[Settings, Depends(get_settings)]
+LlmDep = Annotated[LlmClient, Depends(get_llm)]

+ 0 - 0
algo/src/finrep_algo_agent/api/deps/.gitkeep


+ 0 - 0
algo/src/finrep_algo_agent/api/routers/.gitkeep


+ 5 - 0
algo/src/finrep_algo_agent/api/routers/__init__.py

@@ -0,0 +1,5 @@
+from finrep_algo_agent.api.routers.health import router as health_router
+from finrep_algo_agent.api.routers.outline import router as outline_router
+from finrep_algo_agent.api.routers.section import router as section_router
+
+__all__ = ["health_router", "outline_router", "section_router"]

BIN
algo/src/finrep_algo_agent/api/routers/__pycache__/__init__.cpython-312.pyc


BIN
algo/src/finrep_algo_agent/api/routers/__pycache__/health.cpython-312.pyc


BIN
algo/src/finrep_algo_agent/api/routers/__pycache__/outline.cpython-312.pyc


BIN
algo/src/finrep_algo_agent/api/routers/__pycache__/section.cpython-312.pyc


+ 12 - 0
algo/src/finrep_algo_agent/api/routers/health.py

@@ -0,0 +1,12 @@
+from __future__ import annotations
+
+from fastapi import APIRouter
+
+from finrep_algo_agent import __version__
+
+router = APIRouter(tags=["health"])
+
+
+@router.get("/health")
+def health() -> dict[str, str]:
+    return {"status": "ok", "service": "finrep-algo-agent", "version": __version__}

+ 25 - 0
algo/src/finrep_algo_agent/api/routers/outline.py

@@ -0,0 +1,25 @@
+from __future__ import annotations
+
+from fastapi import APIRouter, HTTPException
+
+from finrep_algo_agent.api.deps import LlmDep, SettingsDep
+from finrep_algo_agent.schemas.outline import OutlineL1Request, OutlineL1Response, OutlineL2Request, OutlineL2Response
+from finrep_algo_agent.skills import run_outline_l1, run_outline_l2
+
+router = APIRouter()
+
+
+@router.post("/l1", response_model=OutlineL1Response)
+async def outline_l1(body: OutlineL1Request, settings: SettingsDep, llm: LlmDep) -> OutlineL1Response:
+    try:
+        return await run_outline_l1(body, settings=settings, llm=llm)
+    except ValueError as e:
+        raise HTTPException(status_code=422, detail=str(e)) from e
+
+
+@router.post("/l2", response_model=OutlineL2Response)
+async def outline_l2(body: OutlineL2Request, settings: SettingsDep, llm: LlmDep) -> OutlineL2Response:
+    try:
+        return await run_outline_l2(body, settings=settings, llm=llm)
+    except ValueError as e:
+        raise HTTPException(status_code=422, detail=str(e)) from e

+ 17 - 0
algo/src/finrep_algo_agent/api/routers/section.py

@@ -0,0 +1,17 @@
+from __future__ import annotations
+
+from fastapi import APIRouter, HTTPException
+
+from finrep_algo_agent.api.deps import LlmDep, SettingsDep
+from finrep_algo_agent.schemas.section import SectionRequest, SectionResponse
+from finrep_algo_agent.skills import run_section
+
+router = APIRouter()
+
+
+@router.post("/section", response_model=SectionResponse)
+async def section(body: SectionRequest, settings: SettingsDep, llm: LlmDep) -> SectionResponse:
+    try:
+        return await run_section(body, settings=settings, llm=llm)
+    except ValueError as e:
+        raise HTTPException(status_code=422, detail=str(e)) from e

+ 0 - 0
algo/src/finrep_algo_agent/config/.gitkeep


+ 3 - 0
algo/src/finrep_algo_agent/config/__init__.py

@@ -0,0 +1,3 @@
+from finrep_algo_agent.config.settings import Settings, get_settings
+
+__all__ = ["Settings", "get_settings"]

BIN
algo/src/finrep_algo_agent/config/__pycache__/__init__.cpython-312.pyc


BIN
algo/src/finrep_algo_agent/config/__pycache__/settings.cpython-312.pyc


+ 27 - 0
algo/src/finrep_algo_agent/config/settings.py

@@ -0,0 +1,27 @@
+from functools import lru_cache
+
+from pydantic import Field
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+
+class Settings(BaseSettings):
+    model_config = SettingsConfigDict(
+        env_prefix="FINREP_",
+        env_file=".env",
+        env_file_encoding="utf-8",
+        extra="ignore",
+    )
+
+    llm_base_url: str = "http://127.0.0.1:9999/v1"
+    llm_api_key: str = ""
+    llm_model: str = "gpt-4o-mini"
+    llm_timeout_seconds: float = 120.0
+    # True:技能返回占位 JSON,不请求 LLM(便于先联调 Java)
+    stub_skills: bool = Field(default=True)
+
+    service_token: str = ""
+
+
+@lru_cache
+def get_settings() -> Settings:
+    return Settings()

+ 0 - 0
algo/src/finrep_algo_agent/core/.gitkeep


+ 3 - 0
algo/src/finrep_algo_agent/llm/__init__.py

@@ -0,0 +1,3 @@
+from finrep_algo_agent.llm.client import LlmClient
+
+__all__ = ["LlmClient"]

BIN
algo/src/finrep_algo_agent/llm/__pycache__/__init__.cpython-312.pyc


BIN
algo/src/finrep_algo_agent/llm/__pycache__/client.cpython-312.pyc


+ 68 - 0
algo/src/finrep_algo_agent/llm/client.py

@@ -0,0 +1,68 @@
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+import httpx
+
+from finrep_algo_agent.config import Settings
+
+logger = logging.getLogger(__name__)
+
+
+class LlmClient:
+    """OpenAI 兼容 `POST {base_url}/chat/completions` 的薄封装。"""
+
+    def __init__(self, settings: Settings) -> None:
+        self._settings = settings
+        base = settings.llm_base_url.rstrip("/")
+        if base.endswith("/v1"):
+            self._chat_url = f"{base}/chat/completions"
+        else:
+            self._chat_url = f"{base}/v1/chat/completions"
+
+    async def chat_completion(
+        self,
+        messages: list[dict[str, str]],
+        *,
+        temperature: float = 0.3,
+        response_format: dict[str, Any] | None = None,
+    ) -> str:
+        payload: dict[str, Any] = {
+            "model": self._settings.llm_model,
+            "messages": messages,
+            "temperature": temperature,
+        }
+        if response_format is not None:
+            payload["response_format"] = response_format
+
+        headers: dict[str, str] = {"Content-Type": "application/json"}
+        if self._settings.llm_api_key:
+            headers["Authorization"] = f"Bearer {self._settings.llm_api_key}"
+
+        timeout = httpx.Timeout(self._settings.llm_timeout_seconds)
+        async with httpx.AsyncClient(timeout=timeout) as client:
+            resp = await client.post(self._chat_url, json=payload, headers=headers)
+            resp.raise_for_status()
+            data = resp.json()
+
+        choices = data.get("choices") or []
+        if not choices:
+            raise ValueError("LLM response missing choices")
+        message = choices[0].get("message") or {}
+        content = message.get("content")
+        if not isinstance(content, str):
+            raise ValueError("LLM response missing message.content")
+        return content
+
+    async def ping(self) -> bool:
+        """发一条极短请求,用于探测连通性。"""
+        try:
+            await self.chat_completion(
+                [{"role": "user", "content": "ping"}],
+                temperature=0,
+            )
+            return True
+        except Exception as e:
+            logger.warning("LLM ping failed: %s", e)
+            return False

+ 0 - 0
algo/src/finrep_algo_agent/llm/client/.gitkeep


+ 0 - 0
algo/src/finrep_algo_agent/llm/providers/.gitkeep


+ 0 - 0
algo/src/finrep_algo_agent/llm/router/.gitkeep


+ 22 - 0
algo/src/finrep_algo_agent/main.py

@@ -0,0 +1,22 @@
+from __future__ import annotations
+
+import logging
+
+from fastapi import FastAPI
+
+from finrep_algo_agent.api.routers import health_router, outline_router, section_router
+
+logging.basicConfig(
+    level=logging.INFO,
+    format="%(asctime)s %(levelname)s %(name)s %(message)s",
+)
+
+app = FastAPI(
+    title="finrep-algo-agent",
+    version="0.1.0",
+    description="财顾报告 Agent — Python 算法服务",
+)
+
+app.include_router(health_router)
+app.include_router(outline_router, prefix="/v1/outline", tags=["outline"])
+app.include_router(section_router, prefix="/v1", tags=["section"])

+ 0 - 0
algo/src/finrep_algo_agent/observability/.gitkeep


+ 11 - 0
algo/src/finrep_algo_agent/prompts/__init__.py

@@ -0,0 +1,11 @@
+from finrep_algo_agent.prompts.builders import (
+    build_outline_l1_user_prompt,
+    build_outline_l2_user_prompt,
+    build_section_user_prompt,
+)
+
+__all__ = [
+    "build_outline_l1_user_prompt",
+    "build_outline_l2_user_prompt",
+    "build_section_user_prompt",
+]

BIN
algo/src/finrep_algo_agent/prompts/__pycache__/__init__.cpython-312.pyc


BIN
algo/src/finrep_algo_agent/prompts/__pycache__/builders.cpython-312.pyc


+ 142 - 0
algo/src/finrep_algo_agent/prompts/builders.py

@@ -0,0 +1,142 @@
+from __future__ import annotations
+
+import json
+from decimal import Decimal
+from pathlib import Path
+
+from jinja2 import Environment, FileSystemLoader, select_autoescape
+
+from finrep_algo_agent.schemas.outline import OutlineL1Request, OutlineL2Request
+from finrep_algo_agent.schemas.section import SectionRequest
+
+_TPL_DIR = Path(__file__).resolve().parent / "templates"
+
+
+def _env() -> Environment:
+    return Environment(
+        loader=FileSystemLoader(_TPL_DIR),
+        autoescape=select_autoescape(disabled_extensions=("j2",)),
+        trim_blocks=True,
+        lstrip_blocks=True,
+    )
+
+
+def _fmt_list(items: list[str] | None) -> str:
+    if not items:
+        return "无"
+    return "、".join(str(x) for x in items)
+
+
+def _fmt_bool(v: bool | None) -> str:
+    if v is True:
+        return "是"
+    if v is False:
+        return "否"
+    return "未提供"
+
+
+def _fmt_decimal(v: Decimal | None) -> str:
+    if v is None:
+        return "未提供"
+    return str(v)
+
+
+def format_chapter_candidates_block(req: OutlineL1Request) -> str:
+    lines: list[str] = []
+    for i, c in enumerate(req.chapter_candidates or [], start=1):
+        row = c.model_dump(exclude_none=True)
+        lines.append(json.dumps(row, ensure_ascii=False))
+    if not lines:
+        return "(空候选清单)"
+    return "\n".join(f"{i}. {line}" for i, line in enumerate(lines, start=1))
+
+
+def format_leaf_chapter_candidates_block(leaf: list[dict]) -> str:
+    if not leaf:
+        return "(空末级候选清单)"
+    return json.dumps(leaf, ensure_ascii=False, indent=2)
+
+
+def _task_background_dict(req: OutlineL1Request) -> dict[str, str]:
+    return {
+        "report_type": req.report_type,
+        "agreement_amount": _fmt_decimal(req.agreement_amount),
+        "enterprise_type": req.enterprise_type or "未提供",
+        "group_business_segments": _fmt_list(req.group_business_segments),
+        "industry_type": req.industry_type or "未提供",
+        "has_independent_report": _fmt_bool(req.has_independent_report),
+        "independent_report_types": _fmt_list(req.independent_report_types),
+        "candidate_financing_tools": _fmt_list(req.candidate_financing_tools),
+        "recommended_financing_tools": _fmt_list(req.recommended_financing_tools),
+        "other_requirements": req.other_requirements or "无",
+    }
+
+
+def build_outline_l1_user_prompt(req: OutlineL1Request) -> str:
+    ctx = _task_background_dict(req)
+    ctx["chapter_candidates_block"] = format_chapter_candidates_block(req)
+    return _env().get_template("outline_l1.j2").render(**ctx)
+
+
+def _outline_l2_background(req: OutlineL2Request) -> OutlineL1Request:
+    if req.l1_task_snapshot is not None:
+        return req.l1_task_snapshot
+    return OutlineL1Request(
+        report_type=req.report_type or "未分类",
+        agreement_amount=req.agreement_amount,
+        enterprise_type=req.enterprise_type,
+        group_business_segments=list(req.group_business_segments),
+        industry_type=req.industry_type,
+        has_independent_report=req.has_independent_report,
+        independent_report_types=list(req.independent_report_types),
+        candidate_financing_tools=list(req.candidate_financing_tools),
+        recommended_financing_tools=list(req.recommended_financing_tools),
+        other_requirements=req.other_requirements,
+        chapter_candidates=[],
+    )
+
+
+def build_outline_l2_user_prompt(req: OutlineL2Request) -> str:
+    ctx = _task_background_dict(_outline_l2_background(req))
+    ctx.update(
+        {
+            "chapter_name": req.chapter_name,
+            "chapter_no": req.chapter_no,
+            "chapter_paragraph_count_enum": req.chapter_paragraph_count_enum or "未提供",
+            "chapter_reason": req.chapter_reason or "无",
+            "overall_logic": req.overall_logic or "无",
+            "leaf_chapter_candidates_block": format_leaf_chapter_candidates_block(
+                req.leaf_chapter_candidates
+            ),
+        }
+    )
+    return _env().get_template("outline_l2.j2").render(**ctx)
+
+
+def _format_json_block(data: dict) -> str:
+    if not data:
+        return "(无)"
+    return json.dumps(data, ensure_ascii=False, indent=2)
+
+
+def build_section_user_prompt(req: SectionRequest) -> str:
+    tt = (req.template_type or "info").lower()
+    name_map = {
+        "info": "section_info.j2",
+        "analysis": "section_analysis.j2",
+        "metric": "section_metric.j2",
+        "judgment": "section_judgment.j2",
+    }
+    tpl_name = name_map.get(tt, "section_info.j2")
+    ctx = {
+        "report_type": req.report_type or "财务顾问",
+        "overall_logic": req.overall_logic or "(未提供整篇报告撰写逻辑,请结合输入自行保持语气一致。)",
+        "chapter_logic": req.chapter_logic or "(未提供一级章节写作逻辑。)",
+        "paragraph_position": req.paragraph_position or "(未提供段落定位说明。)",
+        "task_input_block": _format_json_block(req.task_input),
+        "data_block": _format_json_block(req.data_package),
+        "paragraph_logic": req.paragraph_logic or "(未提供段落撰写逻辑。)",
+        "example_block": req.example or "(无示例)",
+        "notes_block": req.notes or "(无补充注意事项。)",
+    }
+    return _env().get_template(tpl_name).render(**ctx)

+ 0 - 0
algo/src/finrep_algo_agent/prompts/builders/.gitkeep


+ 0 - 0
algo/src/finrep_algo_agent/prompts/templates/.gitkeep


+ 106 - 0
algo/src/finrep_algo_agent/prompts/templates/outline_l1.j2

@@ -0,0 +1,106 @@
+你是一名具有银行投行、对公授信及财务顾问报告经验的专业报告结构规划助手。
+你的任务是:基于输入的报告背景信息、协议金额约束和一级章节候选清单,判断本次报告中每个一级章节是否应当呈现,并为每个章节分配段落数量枚举值,同时输出整篇报告撰写的逻辑说明。你的输出将直接作为系统后续自动处理的输入,因此你必须严格遵守以下要求。
+###
+【任务边界】
+你只负责完成一级章节层面的结构判断,包括:
+判断每个一级章节是否呈现
+为每个一级章节分配段落数量枚举值
+输出整篇报告撰写的逻辑说明
+你不得执行以下内容:
+不得撰写任何正文内容
+不得判断二级章节或最末级知识单元
+不得新增、删除、合并、拆分、改写一级章节名称
+不得输出枚举值以外的数量表达
+不得输出 JSON 结构以外的任何内容
+不得展示你的推理过程或计算过程
+###
+【判断规则】
+1. 判断范围约束
+你只能在下方提供的【一级章节候选清单】中进行判断。
+输出结果必须与输入候选章节一一对应,顺序保持一致,不可缺失,不可新增。
+2.段落数量枚举
+你只能使用以下枚举值:
+P0:不呈现
+P1:1段
+P2:2–5段
+P3:5–10段
+P4:10段及以上
+3.协议金额与整体规模约束
+你必须先根据协议金额判断本次报告整体允许的段落规模区间,再进行各一级章节的数量分配。
+协议金额与目标段落区间规则如下:
+<50万 → 10–20段
+50–100万 → 20–30段
+100–500万 → 30–40段
+>=500万 → 40–50段
+4.你在内部判断时必须同时考虑:
+协议金额对应的总段落预算范围
+各章节重要性
+是否存在独立专项报告
+章节的额外说明 / 知识边界说明
+章节适用条件
+各章节下可供分配的最末级段落统计上限
+报告结构完整性与论证顺序合理性
+但你不得在输出中展示计算过程。
+5. 重要性选择规则
+你必须根据“是否存在独立报告”来选择适用的重要性字段:
+若存在独立报告,则使用“重要性(有独立报告)”
+若不存在独立报告,则使用“重要性(无独立报告)”
+重要性处理原则如下:
+必定:原则上应保留,除非明确不适用
+高:优先保留,但可在总量约束下压缩篇幅
+中、低:可根据整体结构需要压缩或舍弃
+若额外说明中明确指出“存在独立报告时仅需简单介绍”,则即使保留,也应优先分配较低段落级别
+6. 适用条件规则
+若章节存在适用条件,则必须结合输入背景信息判断。
+若条件不满足,则该章节可判断为不呈现。
+若条件满足,也仍需结合重要性、整体规模约束和结构合理性综合判断。
+###
+【输入信息】
+1. 报告背景信息
+报告类型:{{ report_type }}
+协议金额:{{ agreement_amount }}
+企业类型:{{ enterprise_type }}
+集团板块名称:{{ group_business_segments }}
+行业类型:{{ industry_type }}
+是否存在独立调查报告:{{ has_independent_report }}
+独立报告类型:{{ independent_report_types }}
+拟分析融资工具:{{ candidate_financing_tools }}
+拟最终推荐融资工具:{{ recommended_financing_tools }}
+其他要求:{{ other_requirements }}
+2. 一级章节候选清单
+{{ chapter_candidates_block }}
+###
+【输出要求】
+你必须仅输出一个合法的 JSON 对象,不得输出任何其他说明文字、标题、注释、Markdown 标记或代码块标记。
+输出 JSON 结构必须严格如下:
+{
+"chapter_results": [
+{
+"chapter_id": "string",
+"chapter_name": "string",
+"paragraph_count_enum": "P0 or P1 or P2 or P3 or P4",
+"reason": "string"
+}
+],
+"overall_logic": "string"
+}
+###
+【输出约束】
+你必须严格遵守以下输出约束:
+chapter_results 数组长度必须与输入的一级章节候选数量完全一致
+chapter_results 中各元素顺序必须与输入候选顺序完全一致
+chapter_id 和 chapter_name 必须与输入内容原样一致,不得改写
+reason 必须是一句简洁自然语言,只说明该章节是否保留及其段落级别的核心原因
+overall_logic 必须是一段自然语言,只说明一级章节的组织顺序、核心与承接关系、整体结构如何服务最终方案论证
+overall_logic 不得出现具体数量计算过程,不得出现 JSON 之外的层级结构
+输出必须是可被程序直接解析的合法 JSON
+###
+【输出字段说明】
+chapter_results:一级章节判断结果列表,按输入候选章节顺序逐项输出
+chapter_id:一级章节唯一标识,必须与输入一致
+chapter_name:一级章节名称,必须与输入一致
+paragraph_count_enum:章节段落数量枚举,P0 表示不呈现,P1 表示1段,P2 表示2–5段,P3 表示5–10段,P4 表示10段及以上
+reason:该章节判断结果的简要原因说明,使用一句自然语言表达
+overall_logic:输出整篇报告撰写的逻辑说明,用一段自然语言说明章节顺序及整体论证关系
+相关页面示例
+备注说明:目前版本提示词模板已剔除“呈现方式”字段

+ 137 - 0
algo/src/finrep_algo_agent/prompts/templates/outline_l2.j2

@@ -0,0 +1,137 @@
+你是一名具有银行投行、对公授信及财务顾问报告经验的专业报告结构装配助手。
+你的任务是:基于已确定的一级章节约束、输入背景信息和末级章节候选清单,判断当前一级章节下哪些末级章节需要纳入,生成真实报告可用的章节编号结构,并输出一段简要的结构构建逻辑说明。你的输出将直接作为系统后续数据准备和段落生成的输入,因此你必须严格遵守以下要求。
+###
+【任务边界】
+你只负责当前一级章节下的末级章节结构装配,包括:
+判断哪些候选末级章节需要纳入
+按真实报告逻辑生成章节顺序
+生成可直接用于报告拼接的章节编号结构
+对批次处理型章节按输入对象展开子章节
+输出一段结构构建逻辑说明
+你不得执行以下内容:
+不得撰写任何正文内容
+不得输出解释性分析
+不得输出推理过程
+不得新增候选清单中不存在的知识单元
+不得修改输入中已有章节名称
+不得改变候选清单原有层级
+不得输出 JSON 结构以外的任何内容
+###
+【核心判断规则】
+1. 上游约束继承规则
+你必须严格继承上游一级大纲结果,仅对当前一级章节进行处理。
+2. P 枚举使用规则
+上游输入中的段落数量枚举值仅作为规模参考,不是严格上限。若为了保证论证完整性、批次展开或结构闭环,需要超出该规模参考,可以适度扩展。你不得在输出中体现任何数量推演过程。
+P枚举值对应段落数量关系:
+P0:不呈现
+P1:1段
+P2:2–5段
+P3:5–10段
+P4:10段及以上
+3. 重要性优先规则
+你必须根据“是否存在独立报告”选择对应的重要性字段:
+若存在独立报告,则使用“重要性(有独立报告)”
+若不存在独立报告,则使用“重要性(无独立报告)”
+判断原则如下:
+必定:必须纳入
+高:原则上纳入,除非条件不满足或与其他内容明显重复
+低:仅在增强论证闭环、满足输入触发条件或对结构完整性有必要时纳入
+不撰写:必须排除
+4. 条件触发规则
+若候选章节存在备注中的适用条件,则必须结合输入背景信息进行判断。
+若条件不满足,则该候选章节不得纳入。
+若条件满足,则结合重要性和整体结构合理性判断是否纳入。5. 批次处理型展开规则
+若候选章节的“知识单元类型”为 批次处理型,则必须按输入对象逐一展开,并生成独立子章节。
+展开规则如下:
+每个输入对象生成一个独立章节节点
+子章节名称使用输入对象名称
+自动补全对应层级编号
+该类展开不受上游 P 枚举的严格限制
+展开后的章节层级必须保持稳定,不得跳级
+例如:若某候选章节要求按“集团板块名称”展开,则你必须基于输入中的所有集团板块名称逐一生成子章节。
+6. 结构完整性优先规则
+若仅按上游规模参考不足以覆盖关键知识单元或无法形成完整论证闭环,则应优先保证结构完整性,并适度扩展章节数量。
+但不得无依据地纳入大量低重要性内容。
+###
+【章节编号规则】
+你必须生成真实报告可用的章节编号结构。
+编号规则如下:一级章节:1
+二级章节:1.1
+三级章节:1.1.1
+四级章节:1.1.1.1
+你必须遵守以下要求:
+编号必须连续,不得跳号
+顺序必须稳定
+编号层级必须与候选章节原始层级一致
+若为批次处理型展开,可在原有允许层级下生成对应子节点
+不得将低层级内容提升或降级为其他层级
+输出结果必须可直接用于报告拼接
+###
+四、输入信息
+1. 已确定的上游约束
+一级章节名称:{{ chapter_name }}
+一级章节编号:{{ chapter_no }}
+段落数量:{{ chapter_paragraph_count_enum }}
+判断说明:{{ chapter_reason }}
+整体写作逻辑说明:{{ overall_logic }}
+2. 报告背景信息
+报告类型:{{ report_type }}
+协议金额:{{ agreement_amount }}
+企业类型:{{ enterprise_type }}
+集团板块名称:{{ group_business_segments }}
+行业类型:{{ industry_type }}
+是否存在独立调查报告:{{ has_independent_report }}
+独立报告类型:{{ independent_report_types }}
+拟分析融资工具:{{ candidate_financing_tools }}
+拟最终推荐融资工具:{{ recommended_financing_tools }}
+其他要求:{{ other_requirements }}
+3. 最末级章节候选清单
+{{ leaf_chapter_candidates_block }}
+###
+【输出要求】
+你必须仅输出一个合法的 JSON 对象,不得输出任何其他说明文字、标题、注释、Markdown 标记或代码块标记。
+输出 JSON 结构必须严格如下:
+{
+"chapter_name": "string",
+"chapter_no": "string",
+"chapter_structure": [
+{
+"node_id": "string",
+"node_name": "string",
+"node_no": "string",
+"node_level": 2,
+"parent_node_id": "string or null",
+"source_type": "single or batch",
+"source_candidate_name": "string",
+"is_selected": true
+}
+],
+"structure_logic": "string"
+}
+###
+【输出字段说明】
+chapter_name:当前一级章节名称,与输入一致
+chapter_no:当前一级章节编号,与输入一致
+chapter_structure:当前一级章节下最终纳入的章节结构列表
+node_id:章节节点唯一标识
+node_name:章节名称
+node_no:章节编号
+node_level:章节层级,按数字表示
+parent_node_id:父节点ID,一级下直接子节点可为 null 或一级节点ID
+source_type:来源类型,single=单元呈现型,batch=批次处理型
+source_candidate_name:该节点对应的原始候选章节名称
+is_selected:是否纳入,固定为 true
+structure_logic:当前一级章节内部结构构建逻辑说明
+###
+【输出约束】
+你必须严格遵守以下输出约束:
+只输出最终纳入的章节节点,不输出未纳入节点
+chapter_name 和 chapter_no 必须与输入完全一致
+chapter_structure 必须按最终报告顺序输出
+node_name 必须来源于输入候选章节名称或批次展开对象名称,不得随意改写
+source_candidate_name 必须与输入候选章节名称完全一致
+source_type 只能取 single 或 batch
+node_level 必须与实际编号层级一致
+structure_logic 只能写一段自然语言,不得分点,不得写数量,不得新增结构概念
+输出必须是可被程序直接解析的合法 JSON
+相关页面示例

+ 52 - 0
algo/src/finrep_algo_agent/prompts/templates/section_analysis.j2

@@ -0,0 +1,52 @@
+【角色与任务定义】
+你是银行正式{{ report_type }}报告中的正文生成模块,当前负责生成报告中的一个章节段落内容。
+你的任务是:
+围绕当前分析对象,在不进行方案选择、不进行对象之间优劣比较的前提下,基于输入信息与输入数据,对相关对象进行多维度分析与说明,生成可直接用于正式财务顾问报告正文的章节段落内容。
+本模块生成的内容属于报告正文部分,需要保证行文完整、语气审慎、结构清晰,整体风格应符合银行正式财务顾问报告的写作规范。
+###
+【整篇报告撰写思路与整体语境】
+{{ overall_logic }}
+说明:
+该部分用于描述整篇报告在当前项目背景下的总体写作视角、分析逻辑与行文思路,用于保证各章节之间的表达方式保持一致。
+生成本章节内容时必须保持与上述整体语境一致。
+###
+【当前一级章节写作逻辑】
+{{ chapter_logic }}
+说明:
+该部分用于描述当前一级章节在整篇报告中的功能定位,以及该章节在整体报告结构中的作用。生成内容时必须围绕该章节的主题展开,不得偏离章节核心内容。
+###
+【段落定位】
+{{ paragraph_position }}
+说明:用于描述本段落的撰写目标及在整篇报告中的定位说明。
+###
+【输入信息】
+{{ task_input_block }}
+###
+【相关数据】
+{{ data_block }}
+###
+【段落撰写逻辑】
+{{ paragraph_logic }}
+###
+【输出示例(非必须)】
+{{ example_block }}
+说明:示例内容仅用于说明文本结构形式,实际生成内容应结合输入信息进行调整。
+###
+【任务要求】
+●当前需要生成的内容属于一级章节中的具体段落内容,其主要作用是围绕当前分析对象展开系统性说明与分析,为后续方案设计或综合判断提供分析基础。
+●本段落属于分析性内容,需要围绕对象本身展开多维度说明与分析,但不得形成最终结论或方案建议。
+●分析应围绕对象的特征、应用方式、实施条件或相关背景展开,通过连续段落形成完整论述,不得仅进行简单描述。
+###
+【需要特别注意的事项】
+生成内容时必须严格遵守以下要求:
+1、不得进行方案选择:不得明确推荐某一方案或路径。
+2、不得进行对象优劣比较:不得出现“更适合”“优于”“相比之下”等比较性表述。
+3、不得形成最终结论:不得给出明确决策性判断。
+4、不得逐条解释输入信息:输入信息与输入数据应自然融入叙述文本,不得直接按输入结构进行罗列。
+5、不得引入外部信息:不得引用输入信息之外的行业数据、政策条款或其他外部资料。
+6、不得出现系统或规则说明:不得提及模型、规则、判断逻辑、数据处理过程或任何系统行为。
+7、保持正式报告语气:整体语气必须稳健、客观、专业,符合银行正式财务顾问报告的写作规范。
+###
+【其他需要特别注意的事项】
+{{ notes_block }}
+说明:除了上述共性约束外的单独注意事项。

+ 51 - 0
algo/src/finrep_algo_agent/prompts/templates/section_info.j2

@@ -0,0 +1,51 @@
+【角色与任务定义】
+你是银行正式{{ report_type }}报告中的正文生成模块,当前负责生成报告中的一个章节段落内容。
+你的任务是:
+在不进行分析推理、不形成结论判断、不进行方案比较的前提下,基于输入信息与输入数据,对相关情况进行完整、连续、规范的说明性描述,生成可直接用于正式财务顾问报告正文的章节段落内容。
+本模块生成的内容属于报告正文部分,需要保证行文完整、语气审慎、结构清晰,整体风格应符合银行正式财务顾问报告的写作规范。
+###
+【整篇报告撰写思路与整体语境】
+{{ overall_logic }}
+说明:
+该部分用于描述整篇报告在当前项目背景下的总体写作视角、分析逻辑与行文思路,用于保证各章节之间的表达方式保持一致。
+生成本章节内容时必须保持与上述整体语境一致。
+###
+【当前一级章节写作逻辑】
+{{ chapter_logic }}
+说明:
+该部分用于描述当前一级章节在整篇报告中的功能定位,以及该章节在整体报告结构中的作用。生成内容时必须围绕该章节的主题展开,不得偏离章节核心内容。
+###
+【段落定位】
+{{ paragraph_position }}
+说明:用于描述本段落的撰写目标及在整篇报告中的定位说明。
+###
+【输入信息】
+{{ task_input_block }}
+###
+【相关数据】
+{{ data_block }}
+###
+【段落撰写逻辑】
+{{ paragraph_logic }}
+###
+【输出示例(非必须)】
+{{ example_block }}
+说明:示例内容仅用于说明文本结构形式,实际生成内容应结合输入信息进行调整。
+###
+【任务要求】
+当前需要生成的内容属于一级章节中的具体段落内容,其主要作用是对相关情况进行客观说明,为其他分析章节提供基础信息支撑。
+本段落属于说明性内容,仅用于说明事实情况,不承担分析判断或方案设计功能。
+###
+【需要特别注意的事项】
+生成内容时必须严格遵守以下要求:
+1、不得进行分析推理:不得出现“因此”“可以看出”“说明了”“表明”等明显分析性表述。
+2、不得形成结论判断:不得对企业、项目或任何事项作出评价性结论。
+3、不得进行方案比较:不得出现不同对象之间的比较描述。
+4、不得逐条解释输入信息:输入信息与输入数据应自然融入叙述文本,不得直接按输入结构进行罗列。
+5、不得引入外部信息:不得引用输入信息之外的行业数据、政策条款或其他外部资料。
+6、不得出现系统或规则说明:不得提及模型、规则、判断逻辑、数据处理过程或任何系统行为。
+7、保持正式报告语气:整体语气必须稳健、客观、专业,符合银行正式财务顾问报告的写作风格。
+###
+【其他需要特别注意的事项】
+{{ notes_block }}
+说明:除了上述共性约束外的单独注意事项。

+ 53 - 0
algo/src/finrep_algo_agent/prompts/templates/section_judgment.j2

@@ -0,0 +1,53 @@
+【角色与任务定义】
+你是银行正式{{ report_type }}报告中的正文生成模块,当前负责生成报告中的一个章节段落内容。
+你的任务是:
+在既定方案或既定结论基础上,对相关安排进行系统性论述和合理性说明,生成可直接用于正式财务顾问报告正文的章节段落内容。
+本模块生成的内容属于报告正文部分,需要保证行文完整、语气审慎、结构清晰,整体风格应符合银行正式财务顾问报告的写作规范。
+###
+【整篇报告撰写思路与整体语境】
+{{ overall_logic }}
+说明:
+该部分用于描述整篇报告在当前项目背景下的总体写作视角、分析逻辑与行文思路,用于保证各章节之间的表达方式保持一致。
+生成本章节内容时必须保持与上述整体语境一致。
+###
+【当前一级章节写作逻辑】
+{{ chapter_logic }}
+说明:
+该部分用于描述当前一级章节在整篇报告中的功能定位,以及该章节在整体报告结构中的作用。生成内容时必须围绕该章节的主题展开,不得偏离章节核心内容。
+###
+【段落定位】
+{{ paragraph_position }}
+说明:用于描述本段落的撰写目标及在整篇报告中的定位说明。
+###
+【输入信息】
+{{ task_input_block }}
+###
+【相关数据】
+{{ data_block }}
+###
+【段落撰写逻辑】
+{{ paragraph_logic }}
+###
+【输出示例(非必须)】
+{{ example_block }}
+说明:示例内容仅用于说明文本结构形式,实际生成内容应结合输入信息进行调整。
+###
+【任务要求】
+●当前需要生成的内容属于一级章节中的具体段落内容,其主要作用是在既定方案或既定安排基础上,对相关方案的合理性与实施逻辑进行系统说明。
+●本段落属于综合判断型内容,应围绕既定方案或既定安排展开论述,通过连续段落对方案背景、实施逻辑及结构安排进行完整说明。
+●内容应重点说明该方案在当前项目条件下的适配性、实施基础以及整体安排逻辑,但不得改变既定方案或新增其他方案。
+###
+【需要特别注意的事项】
+生成内容时必须严格遵守以下要求:
+1、不得改变既定方案:必须基于输入信息中已经确定的方案或安排进行说明,不得新增、替换或删除方案。
+2、不得进行方案选择过程说明:不得描述筛选逻辑、比较过程或规则判断过程。
+3、不得进行方案之间的比较:不得出现“更适合”“优于”“相比之下”等比较性表述。
+4、不得引入新的决策判断:不得提出新的融资工具、融资路径或方案设计。
+5、不得逐条解释输入信息:输入信息与输入数据应自然融入叙述文本,不得直接按输入结构进行罗列。
+6、不得引入外部信息:不得引用输入信息之外的行业数据、政策条款或其他外部资料。
+7、不得出现系统或规则说明:不得提及模型、规则、判断逻辑、数据处理过程或任何系统行为。
+8、保持正式报告语气:整体语气必须稳健、客观、专业,符合银行正式财务顾问报告的写作风格。
+###
+【其他需要特别注意的事项】
+{{ notes_block }}
+说明:除了上述共性约束外的单独注意事项。

+ 53 - 0
algo/src/finrep_algo_agent/prompts/templates/section_metric.j2

@@ -0,0 +1,53 @@
+【角色与任务定义】
+你是银行正式{{ report_type }}报告中的正文生成模块,当前负责生成报告中的一个章节段落内容。
+你的任务是:
+基于输入信息与输入数据中的相关指标,对企业或项目相关情况进行客观的数据情况描述,生成可直接用于正式财务顾问报告正文的章节段落内容。
+本模块生成的内容属于报告正文部分,需要保证行文完整、语气审慎、结构清晰,整体风格应符合银行正式财务顾问报告的写作规范。
+###
+【整篇报告撰写思路与整体语境】
+{{ overall_logic }}
+说明:
+该部分用于描述整篇报告在当前项目背景下的总体写作视角、分析逻辑与行文思路,用于保证各章节之间的表达方式保持一致。
+生成本章节内容时必须保持与上述整体语境一致。
+###
+【当前一级章节写作逻辑】
+{{ chapter_logic }}
+说明:
+该部分用于描述当前一级章节在整篇报告中的功能定位,以及该章节在整体报告结构中的作用。生成内容时必须围绕该章节的主题展开,不得偏离章节核心内容。
+###
+【段落定位】
+{{ paragraph_position }}
+说明:用于描述本段落的撰写目标及在整篇报告中的定位说明。
+###
+【输入信息】
+{{ task_input_block }}
+###
+【相关数据】
+{{ data_block }}
+###
+【段落撰写逻辑】
+{{ paragraph_logic }}
+###
+【输出示例(非必须)】
+{{ example_block }}
+说明:示例内容仅用于说明文本结构形式,实际生成内容应结合输入信息进行调整。
+###
+【任务要求】
+●当前需要生成的内容属于一级章节中的具体段落内容,其主要作用是基于相关指标数据,对企业或项目的相关数据情况进行客观描述。
+●本段落属于数据描述型内容,应围绕输入数据中的指标信息,对指标规模、结构特征或变化情况进行说明。
+●内容应重点呈现数据情况本身,通过连续段落进行描述,但不得对数据形成原因解释或经营判断。
+###
+【需要特别注意的事项】
+生成内容时必须严格遵守以下要求:
+1、不得编造数据:所有涉及指标的描述必须基于输入数据,不得虚构或补充未提供的数据。
+2、不得进行原因分析:不得解释指标变化的原因,不得进行归因分析。
+3、不得形成经营判断:不得出现“说明企业经营良好”“体现企业盈利能力提升”等评价性表述。
+4、不得进行趋势推断:不得对未来情况进行预测或判断。
+5、不得逐条解释输入信息:输入信息与输入数据应自然融入叙述文本,不得直接按输入结构进行罗列。
+6、不得引入外部信息:不得引用输入信息之外的行业数据、政策条款或其他外部资料。
+7、不得出现系统或规则说明:不得提及模型、规则、判断逻辑、数据处理过程或任何系统行为。
+8、保持正式报告语气:整体语气必须稳健、客观、专业,符合银行正式财务顾问报告的写作风格。
+###
+【其他需要特别注意的事项】
+{{ notes_block }}
+说明:除了上述共性约束外的单独注意事项。

+ 0 - 0
algo/src/finrep_algo_agent/rag/ingestion/.gitkeep


+ 0 - 0
algo/src/finrep_algo_agent/rag/retrieval/.gitkeep


+ 0 - 0
algo/src/finrep_algo_agent/rag/vectorstore/.gitkeep


+ 0 - 0
algo/src/finrep_algo_agent/schemas/.gitkeep


+ 23 - 0
algo/src/finrep_algo_agent/schemas/__init__.py

@@ -0,0 +1,23 @@
+from finrep_algo_agent.schemas.outline import (
+    ChapterCandidate,
+    ChapterL1Result,
+    ChapterStructureNode,
+    OutlineL1Request,
+    OutlineL1Response,
+    OutlineL2Request,
+    OutlineL2Response,
+)
+from finrep_algo_agent.schemas.section import SectionRequest, SectionResponse, TokenUsage
+
+__all__ = [
+    "ChapterCandidate",
+    "ChapterL1Result",
+    "ChapterStructureNode",
+    "OutlineL1Request",
+    "OutlineL1Response",
+    "OutlineL2Request",
+    "OutlineL2Response",
+    "SectionRequest",
+    "SectionResponse",
+    "TokenUsage",
+]

BIN
algo/src/finrep_algo_agent/schemas/__pycache__/__init__.cpython-312.pyc


BIN
algo/src/finrep_algo_agent/schemas/__pycache__/outline.cpython-312.pyc


BIN
algo/src/finrep_algo_agent/schemas/__pycache__/section.cpython-312.pyc


+ 89 - 0
algo/src/finrep_algo_agent/schemas/outline.py

@@ -0,0 +1,89 @@
+from __future__ import annotations
+
+from decimal import Decimal
+from typing import Any
+
+from pydantic import BaseModel, ConfigDict, Field
+
+
+class ChapterCandidate(BaseModel):
+    """一级章节候选;额外键(重要性、适用条件等)原样透传大模型。"""
+
+    model_config = ConfigDict(extra="allow")
+
+    chapter_id: str = ""
+    chapter_name: str = ""
+
+
+class OutlineL1Request(BaseModel):
+    """一级大纲:与 Java 侧对齐的主要字段;其余可放 extra 或后续补全。"""
+
+    task_id: str | None = None
+    tenant_id: str | None = None
+    report_type: str
+    agreement_amount: Decimal | None = None
+    enterprise_type: str | None = None
+    group_business_segments: list[str] = Field(default_factory=list)
+    industry_type: str | None = None
+    has_independent_report: bool | None = None
+    independent_report_types: list[str] = Field(default_factory=list)
+    candidate_financing_tools: list[str] = Field(default_factory=list)
+    recommended_financing_tools: list[str] = Field(default_factory=list)
+    other_requirements: str | None = None
+    chapter_candidates: list[ChapterCandidate] = Field(default_factory=list)
+
+
+class ChapterL1Result(BaseModel):
+    chapter_id: str
+    chapter_name: str
+    paragraph_count_enum: str = Field(description="P0~P4")
+    reason: str = ""
+
+
+class OutlineL1Response(BaseModel):
+    chapter_results: list[ChapterL1Result]
+    overall_logic: str = ""
+
+
+class OutlineL2Request(BaseModel):
+    task_id: str | None = None
+    tenant_id: str | None = None
+    chapter_name: str
+    chapter_no: str
+    chapter_paragraph_count_enum: str | None = None
+    """一级大纲中该章的 reason,对应需求文档「判断说明」。"""
+    chapter_reason: str = ""
+    """一级大纲 overall_logic,对应「整体写作逻辑说明」。"""
+    overall_logic: str = ""
+    leaf_chapter_candidates: list[dict[str, Any]] = Field(default_factory=list)
+    """可选:整段 L1 任务快照;若为空则用本对象下列字段拼报告背景。"""
+    l1_task_snapshot: OutlineL1Request | None = None
+    report_type: str | None = None
+    agreement_amount: Decimal | None = None
+    enterprise_type: str | None = None
+    group_business_segments: list[str] = Field(default_factory=list)
+    industry_type: str | None = None
+    has_independent_report: bool | None = None
+    independent_report_types: list[str] = Field(default_factory=list)
+    candidate_financing_tools: list[str] = Field(default_factory=list)
+    recommended_financing_tools: list[str] = Field(default_factory=list)
+    other_requirements: str | None = None
+    l1_context: dict[str, Any] | None = None
+
+
+class ChapterStructureNode(BaseModel):
+    node_id: str
+    node_name: str
+    node_no: str
+    node_level: int
+    parent_node_id: str | None = None
+    source_type: str | None = None
+    source_candidate_name: str | None = None
+    is_selected: bool = True
+
+
+class OutlineL2Response(BaseModel):
+    chapter_name: str
+    chapter_no: str
+    chapter_structure: list[ChapterStructureNode]
+    structure_logic: str = ""

+ 31 - 0
algo/src/finrep_algo_agent/schemas/section.py

@@ -0,0 +1,31 @@
+from __future__ import annotations
+
+from typing import Any
+
+from pydantic import BaseModel, Field
+
+
+class SectionRequest(BaseModel):
+    task_id: str | None = None
+    tenant_id: str | None = None
+    knowledge_unit_id: str
+    report_type: str | None = None
+    template_type: str = Field(description="info|analysis|metric|judgment")
+    paragraph_logic: str = ""
+    paragraph_position: str = ""
+    overall_logic: str = ""
+    chapter_logic: str = ""
+    task_input: dict[str, Any] = Field(default_factory=dict)
+    data_package: dict[str, Any] = Field(default_factory=dict)
+    example: str = ""
+    notes: str = ""
+
+
+class TokenUsage(BaseModel):
+    prompt_tokens: int = 0
+    completion_tokens: int = 0
+
+
+class SectionResponse(BaseModel):
+    generated_text: str
+    usage: TokenUsage = Field(default_factory=TokenUsage)

+ 0 - 0
algo/src/finrep_algo_agent/services/.gitkeep


+ 5 - 0
algo/src/finrep_algo_agent/skills/__init__.py

@@ -0,0 +1,5 @@
+from finrep_algo_agent.skills.outline_l1 import run_outline_l1
+from finrep_algo_agent.skills.outline_l2 import run_outline_l2
+from finrep_algo_agent.skills.section_gen import run_section
+
+__all__ = ["run_outline_l1", "run_outline_l2", "run_section"]

BIN
algo/src/finrep_algo_agent/skills/__pycache__/__init__.cpython-312.pyc


BIN
algo/src/finrep_algo_agent/skills/__pycache__/outline_l1.cpython-312.pyc


BIN
algo/src/finrep_algo_agent/skills/__pycache__/outline_l2.cpython-312.pyc


BIN
algo/src/finrep_algo_agent/skills/__pycache__/section_gen.cpython-312.pyc


+ 75 - 0
algo/src/finrep_algo_agent/skills/outline_l1.py

@@ -0,0 +1,75 @@
+from __future__ import annotations
+
+import logging
+
+from finrep_algo_agent.config import Settings
+from finrep_algo_agent.llm import LlmClient
+from finrep_algo_agent.prompts import build_outline_l1_user_prompt
+from finrep_algo_agent.schemas.outline import (
+    ChapterCandidate,
+    ChapterL1Result,
+    OutlineL1Request,
+    OutlineL1Response,
+)
+
+logger = logging.getLogger(__name__)
+
+
+def _stub_l1(req: OutlineL1Request) -> OutlineL1Response:
+    results: list[ChapterL1Result] = []
+    candidates: list[ChapterCandidate] = req.chapter_candidates or [
+        ChapterCandidate(chapter_id="stub-1", chapter_name="占位章节"),
+    ]
+    for i, c in enumerate(candidates):
+        cid = c.chapter_id or f"ch-{i+1}"
+        cname = c.chapter_name or f"章节{i+1}"
+        results.append(
+            ChapterL1Result(
+                chapter_id=str(cid),
+                chapter_name=str(cname),
+                paragraph_count_enum="P2",
+                reason="stub:占位理由",
+            )
+        )
+    return OutlineL1Response(
+        chapter_results=results,
+        overall_logic="stub:本响应为占位数据(FINREP_STUB_SKILLS=true)。",
+    )
+
+
+def _validate_l1_against_request(req: OutlineL1Request, resp: OutlineL1Response) -> None:
+    cands = req.chapter_candidates or []
+    if len(resp.chapter_results) != len(cands):
+        raise ValueError(
+            f"chapter_results 数量({len(resp.chapter_results)})必须与候选章节数量({len(cands)})一致"
+        )
+    for got, exp in zip(resp.chapter_results, cands, strict=True):
+        if got.chapter_id != exp.chapter_id or got.chapter_name != exp.chapter_name:
+            raise ValueError(
+                f"chapter_id/chapter_name 必须与输入原样一致:期望 {exp.chapter_id}/{exp.chapter_name},"
+                f"实际 {got.chapter_id}/{got.chapter_name}"
+            )
+
+
+async def run_outline_l1(
+    req: OutlineL1Request,
+    *,
+    settings: Settings,
+    llm: LlmClient,
+) -> OutlineL1Response:
+    if settings.stub_skills:
+        return _stub_l1(req)
+
+    user_content = build_outline_l1_user_prompt(req)
+    raw = await llm.chat_completion(
+        [{"role": "user", "content": user_content}],
+        temperature=0.2,
+        response_format={"type": "json_object"},
+    )
+    try:
+        out = OutlineL1Response.model_validate_json(raw)
+        _validate_l1_against_request(req, out)
+        return out
+    except Exception as e:
+        logger.exception("L1 JSON parse/validate failed")
+        raise ValueError(f"Invalid L1 LLM output: {e}") from e

+ 0 - 0
algo/src/finrep_algo_agent/skills/outline_l1/.gitkeep


+ 65 - 0
algo/src/finrep_algo_agent/skills/outline_l2.py

@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+import logging
+
+from finrep_algo_agent.config import Settings
+from finrep_algo_agent.llm import LlmClient
+from finrep_algo_agent.prompts import build_outline_l2_user_prompt
+from finrep_algo_agent.schemas.outline import (
+    ChapterStructureNode,
+    OutlineL2Request,
+    OutlineL2Response,
+)
+
+logger = logging.getLogger(__name__)
+
+
+def _stub_l2(req: OutlineL2Request) -> OutlineL2Response:
+    base_id = f"{req.chapter_no}-ku1"
+    nodes = [
+        ChapterStructureNode(
+            node_id=base_id,
+            node_name=f"{req.chapter_name}-知识单元(占位)",
+            node_no=f"{req.chapter_no}.1",
+            node_level=2,
+            parent_node_id=None,
+            source_type="single",
+            source_candidate_name=None,
+            is_selected=True,
+        ),
+    ]
+    return OutlineL2Response(
+        chapter_name=req.chapter_name,
+        chapter_no=req.chapter_no,
+        chapter_structure=nodes,
+        structure_logic="stub:末级知识单元占位(FINREP_STUB_SKILLS=true)。",
+    )
+
+
+async def run_outline_l2(
+    req: OutlineL2Request,
+    *,
+    settings: Settings,
+    llm: LlmClient,
+) -> OutlineL2Response:
+    if settings.stub_skills:
+        return _stub_l2(req)
+
+    user_content = build_outline_l2_user_prompt(req)
+    raw = await llm.chat_completion(
+        [{"role": "user", "content": user_content}],
+        temperature=0.2,
+        response_format={"type": "json_object"},
+    )
+    try:
+        parsed = OutlineL2Response.model_validate_json(raw)
+        if parsed.chapter_name != req.chapter_name or parsed.chapter_no != req.chapter_no:
+            raise ValueError(
+                "模型返回的 chapter_name/chapter_no 与请求不一致,"
+                f"期望 {req.chapter_name}/{req.chapter_no},"
+                f"实际 {parsed.chapter_name}/{parsed.chapter_no}"
+            )
+        return parsed
+    except Exception as e:
+        logger.exception("L2 JSON parse/validate failed")
+        raise ValueError(f"Invalid L2 LLM output: {e}") from e

+ 0 - 0
algo/src/finrep_algo_agent/skills/outline_l2/.gitkeep


+ 0 - 0
algo/src/finrep_algo_agent/skills/rag_retrieve/.gitkeep


+ 39 - 0
algo/src/finrep_algo_agent/skills/section_gen.py

@@ -0,0 +1,39 @@
+from __future__ import annotations
+
+import logging
+
+from finrep_algo_agent.config import Settings
+from finrep_algo_agent.llm import LlmClient
+from finrep_algo_agent.prompts import build_section_user_prompt
+from finrep_algo_agent.schemas.section import SectionRequest, SectionResponse, TokenUsage
+
+logger = logging.getLogger(__name__)
+
+
+def _stub_section(req: SectionRequest) -> SectionResponse:
+    text = (
+        f"【占位正文】knowledge_unit_id={req.knowledge_unit_id} "
+        f"template_type={req.template_type}\n"
+        "(FINREP_STUB_SKILLS=true 时不调用模型。)"
+    )
+    return SectionResponse(generated_text=text, usage=TokenUsage())
+
+
+async def run_section(
+    req: SectionRequest,
+    *,
+    settings: Settings,
+    llm: LlmClient,
+) -> SectionResponse:
+    if settings.stub_skills:
+        return _stub_section(req)
+
+    user = build_section_user_prompt(req)
+    text = await llm.chat_completion(
+        [{"role": "user", "content": user}],
+        temperature=0.4,
+    )
+    return SectionResponse(
+        generated_text=text.strip(),
+        usage=TokenUsage(),
+    )

+ 0 - 0
algo/src/finrep_algo_agent/skills/section_gen/.gitkeep


+ 0 - 0
algo/tests/__init__.py


BIN
algo/tests/__pycache__/__init__.cpython-312.pyc


BIN
algo/tests/__pycache__/test_health.cpython-312-pytest-9.0.2.pyc


BIN
algo/tests/__pycache__/test_prompts.cpython-312-pytest-9.0.2.pyc


+ 0 - 0
algo/tests/integration/.gitkeep


+ 32 - 0
algo/tests/test_health.py

@@ -0,0 +1,32 @@
+from fastapi.testclient import TestClient
+
+from finrep_algo_agent.main import app
+
+client = TestClient(app)
+
+
+def test_health() -> None:
+    r = client.get("/health")
+    assert r.status_code == 200
+    data = r.json()
+    assert data["status"] == "ok"
+
+
+def test_outline_l1_stub() -> None:
+    r = client.post(
+        "/v1/outline/l1",
+        json={"report_type": "项目融资", "chapter_candidates": [{"chapter_id": "a", "chapter_name": "测试章"}]},
+    )
+    assert r.status_code == 200
+    body = r.json()
+    assert "chapter_results" in body
+    assert len(body["chapter_results"]) >= 1
+
+
+def test_section_stub() -> None:
+    r = client.post(
+        "/v1/section",
+        json={"knowledge_unit_id": "ku-1", "template_type": "info"},
+    )
+    assert r.status_code == 200
+    assert "generated_text" in r.json()

+ 59 - 0
algo/tests/test_prompts.py

@@ -0,0 +1,59 @@
+from decimal import Decimal
+
+from finrep_algo_agent.prompts.builders import (
+    build_outline_l1_user_prompt,
+    build_outline_l2_user_prompt,
+    build_section_user_prompt,
+)
+from finrep_algo_agent.schemas.outline import ChapterCandidate, OutlineL1Request, OutlineL2Request
+from finrep_algo_agent.schemas.section import SectionRequest
+
+
+def test_outline_l1_template_renders() -> None:
+    req = OutlineL1Request(
+        report_type="项目融资",
+        agreement_amount=Decimal("100"),
+        chapter_candidates=[ChapterCandidate(chapter_id="c1", chapter_name="测试章")],
+    )
+    text = build_outline_l1_user_prompt(req)
+    assert "项目融资" in text
+    assert "c1" in text
+    assert "一级章节候选清单" in text
+
+
+def test_outline_l2_template_renders() -> None:
+    l1 = OutlineL1Request(
+        report_type="项目融资",
+        agreement_amount=Decimal("50"),
+        chapter_candidates=[],
+    )
+    req = OutlineL2Request(
+        chapter_name="融资方案",
+        chapter_no="3",
+        chapter_paragraph_count_enum="P2",
+        chapter_reason="保留",
+        overall_logic="总逻辑",
+        leaf_chapter_candidates=[{"name": "末级1", "知识单元类型": "单元呈现型"}],
+        l1_task_snapshot=l1,
+    )
+    text = build_outline_l2_user_prompt(req)
+    assert "融资方案" in text
+    assert "末级1" in text
+    assert "判断说明" in text
+
+
+def test_section_template_renders() -> None:
+    req = SectionRequest(
+        knowledge_unit_id="ku1",
+        report_type="财务顾问",
+        template_type="info",
+        overall_logic="整体",
+        chapter_logic="章逻辑",
+        paragraph_position="定位",
+        task_input={"a": 1},
+        data_package={"b": 2},
+        paragraph_logic="撰写",
+    )
+    text = build_section_user_prompt(req)
+    assert "财务顾问" in text
+    assert "撰写" in text

+ 0 - 0
algo/tests/unit/.gitkeep


+ 0 - 0
backend/docs/.gitkeep


+ 0 - 0
backend/finrep-application/src/main/java/com/yuxin/finrep/application/command/.gitkeep


+ 0 - 0
backend/finrep-application/src/main/java/com/yuxin/finrep/application/orchestration/.gitkeep


+ 0 - 0
backend/finrep-application/src/main/java/com/yuxin/finrep/application/policy/.gitkeep


+ 0 - 0
backend/finrep-application/src/main/java/com/yuxin/finrep/application/port/in/.gitkeep


+ 0 - 0
backend/finrep-application/src/main/java/com/yuxin/finrep/application/port/out/.gitkeep


+ 0 - 0
backend/finrep-application/src/main/java/com/yuxin/finrep/application/query/.gitkeep


+ 0 - 0
backend/finrep-application/src/main/java/com/yuxin/finrep/application/service/.gitkeep


+ 0 - 0
backend/finrep-application/src/main/resources/.gitkeep


+ 0 - 0
backend/finrep-application/src/test/java/com/yuxin/finrep/application/.gitkeep


+ 0 - 0
backend/finrep-common/src/main/java/com/yuxin/finrep/common/constant/.gitkeep


Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov