angineer-core 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. angineer_core-0.1.0/LICENSE +21 -0
  2. angineer_core-0.1.0/PKG-INFO +245 -0
  3. angineer_core-0.1.0/README.md +215 -0
  4. angineer_core-0.1.0/pyproject.toml +60 -0
  5. angineer_core-0.1.0/setup.cfg +4 -0
  6. angineer_core-0.1.0/src/angineer_core/__init__.py +91 -0
  7. angineer_core-0.1.0/src/angineer_core/agent_configs.py +823 -0
  8. angineer_core-0.1.0/src/angineer_core/agent_events.py +19 -0
  9. angineer_core-0.1.0/src/angineer_core/agent_loop.py +1642 -0
  10. angineer_core-0.1.0/src/angineer_core/agent_messages.py +305 -0
  11. angineer_core-0.1.0/src/angineer_core/agent_policy.py +197 -0
  12. angineer_core-0.1.0/src/angineer_core/agent_session.py +105 -0
  13. angineer_core-0.1.0/src/angineer_core/agent_tools.py +1377 -0
  14. angineer_core-0.1.0/src/angineer_core/base_config.py +178 -0
  15. angineer_core-0.1.0/src/angineer_core/base_contracts.py +216 -0
  16. angineer_core-0.1.0/src/angineer_core/base_di.py +42 -0
  17. angineer_core-0.1.0/src/angineer_core/base_logger.py +167 -0
  18. angineer_core-0.1.0/src/angineer_core/base_utils.py +12 -0
  19. angineer_core-0.1.0/src/angineer_core/canonical_table_lookup.py +139 -0
  20. angineer_core-0.1.0/src/angineer_core/classifier.py +1147 -0
  21. angineer_core-0.1.0/src/angineer_core/contracts.py +53 -0
  22. angineer_core-0.1.0/src/angineer_core/docs_retrieval_client.py +166 -0
  23. angineer_core-0.1.0/src/angineer_core/history_store.py +57 -0
  24. angineer_core-0.1.0/src/angineer_core/memory.py +236 -0
  25. angineer_core-0.1.0/src/angineer_core/ops_metrics.py +85 -0
  26. angineer_core-0.1.0/src/angineer_core/policy_query.py +333 -0
  27. angineer_core-0.1.0/src/angineer_core/ports.py +149 -0
  28. angineer_core-0.1.0/src/angineer_core/prompts/__init__.py +53 -0
  29. angineer_core-0.1.0/src/angineer_core/prompts/agent_configs.py +363 -0
  30. angineer_core-0.1.0/src/angineer_core/prompts/answer_eval.py +40 -0
  31. angineer_core-0.1.0/src/angineer_core/prompts/classifier.py +96 -0
  32. angineer_core-0.1.0/src/angineer_core/prompts/dispatcher.py +121 -0
  33. angineer_core-0.1.0/src/angineer_core/prompts/evals_routes.py +31 -0
  34. angineer_core-0.1.0/src/angineer_core/prompts/retrieval.py +58 -0
  35. angineer_core-0.1.0/src/angineer_core/prompts/sop_routes.py +18 -0
  36. angineer_core-0.1.0/src/angineer_core/qa_pipeline.py +10 -0
  37. angineer_core-0.1.0/src/angineer_core/retrieval_pipeline.py +555 -0
  38. angineer_core-0.1.0/src/angineer_core/run_manifest.py +41 -0
  39. angineer_core-0.1.0/src/angineer_core/sop_runner.py +1216 -0
  40. angineer_core-0.1.0/src/angineer_core/table_query_engine.py +623 -0
  41. angineer_core-0.1.0/src/angineer_core/tool_codec.py +194 -0
  42. angineer_core-0.1.0/src/angineer_core/trace_collector.py +27 -0
  43. angineer_core-0.1.0/src/angineer_core.egg-info/PKG-INFO +245 -0
  44. angineer_core-0.1.0/src/angineer_core.egg-info/SOURCES.txt +55 -0
  45. angineer_core-0.1.0/src/angineer_core.egg-info/dependency_links.txt +1 -0
  46. angineer_core-0.1.0/src/angineer_core.egg-info/requires.txt +10 -0
  47. angineer_core-0.1.0/src/angineer_core.egg-info/top_level.txt +1 -0
  48. angineer_core-0.1.0/tests/test_answer_format_injection.py +63 -0
  49. angineer_core-0.1.0/tests/test_context_topn.py +57 -0
  50. angineer_core-0.1.0/tests/test_entity_search_dual_track.py +170 -0
  51. angineer_core-0.1.0/tests/test_evidence_pointers.py +225 -0
  52. angineer_core-0.1.0/tests/test_meta_query.py +266 -0
  53. angineer_core-0.1.0/tests/test_multi_library_client.py +39 -0
  54. angineer_core-0.1.0/tests/test_multi_library_policy.py +81 -0
  55. angineer_core-0.1.0/tests/test_multi_library_scope.py +44 -0
  56. angineer_core-0.1.0/tests/test_multi_library_tools.py +138 -0
  57. angineer_core-0.1.0/tests/test_search_memo.py +264 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rowe Smith
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,245 @@
1
+ Metadata-Version: 2.4
2
+ Name: angineer-core
3
+ Version: 0.1.0
4
+ Summary: AnGIneer core service - AI Engineer Operating System Core Module
5
+ Author: AnGIneer Team
6
+ License: MIT
7
+ Project-URL: Repository, https://github.com/0mao0/angineer-core
8
+ Project-URL: Issues, https://github.com/0mao0/angineer-core/issues
9
+ Keywords: ai,agent,sop,engineering
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: angineer-ai-inference<0.3.0,>=0.2.2
21
+ Requires-Dist: jsonschema>=4.0.0
22
+ Requires-Dist: pydantic<3.0.0,>=2.0.0
23
+ Requires-Dist: python-dotenv>=1.0.0
24
+ Requires-Dist: requests>=2.31.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
27
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
28
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # angineer-core
32
+
33
+ [![PyPI](https://img.shields.io/pypi/v/angineer-core)](https://pypi.org/project/angineer-core/)
34
+
35
+ AnGIneer 的问答编排内核(Agent Harness 引擎,纯 Python 库,非服务):一条
36
+ **意图分级 → 策略展开 → 多轮工具调用 → 证据守卫 → 出处标注** 的完整链路。
37
+
38
+ > 定位:本库**不含 HTTP 服务、不含知识库存储、不含检索配方**。
39
+ > LLM 调用交给 [angineer-ai-inference](https://pypi.org/project/angineer-ai-inference/);
40
+ > 检索实现由消费方注入——要么通过端口注册挂进本进程,要么配 `ANGINEER_DOCS_API_URL` 走 HTTP。
41
+
42
+ ## 功能架构
43
+
44
+ ```mermaid
45
+ flowchart TB
46
+ subgraph entry["入口"]
47
+ pq["run_policy_query(同步、无 HTTP 依赖)"]
48
+ loop["run_agent_loop(自备配置的裸循环)"]
49
+ end
50
+
51
+ subgraph harness["编排"]
52
+ cls["意图分级 IntentClassifier(L0~L4)"]
53
+ pol["策略展开 build_attempts(Attempt 状态机)"]
54
+ guard["证据守卫 make_final_answer_guard"]
55
+ codec["工具文本协议 TextToolCallCodec"]
56
+ end
57
+
58
+ subgraph tools["工具箱"]
59
+ ks["knowledge_search"]
60
+ ts["table_search"]
61
+ es["entity_search"]
62
+ st["knowledge_stats"]
63
+ calc["calculator / sop_execute / conditional"]
64
+ end
65
+
66
+ subgraph seams["检索接入(二选一)"]
67
+ http["HTTP:ANGINEER_DOCS_API_URL(docs-api 契约)"]
68
+ ports["端口注入:ports.register_agent_search 等"]
69
+ end
70
+
71
+ subgraph llm["LLM"]
72
+ ai["angineer-ai-inference(LLM_CONFIGS 多模型路由)"]
73
+ end
74
+
75
+ pq --> cls --> pol --> loop
76
+ loop --> codec
77
+ codec --> tools
78
+ tools --> seams
79
+ loop --> llm
80
+ llm --> guard
81
+ guard --> pq
82
+ ```
83
+
84
+ ## 安装
85
+
86
+ ```bash
87
+ # PyPI(推荐)
88
+ pip install angineer-core
89
+
90
+ # 或从 GitHub 钉版本安装
91
+ pip install "angineer-core @ git+https://github.com/0mao0/angineer-core.git@v0.1.0"
92
+
93
+ # 本地开发(主仓库 AnGIneer 内)
94
+ pip install -e services/angineer-core
95
+ ```
96
+
97
+ Python 要求 `>=3.10`;依赖:`angineer-ai-inference`、`pydantic>=2.0,<3.0`、
98
+ `python-dotenv>=1.0`、`requests>=2.31`、`jsonschema`。
99
+
100
+ ## 快速开始
101
+
102
+ 两步就位:配 LLM(`LLM_CONFIGS`,格式见 [angineer-ai-inference](https://pypi.org/project/angineer-ai-inference/)),
103
+ 再指一条检索来源——下面是走 HTTP 的最小例子。
104
+
105
+ ```bash
106
+ export LLM_CONFIGS='[{"name":"Qwen3.6-A3B","model":"Qwen3.6-35B-A3B-FP8","api_key":"sk-...","base_url":"https://your-gateway/v1","enabled":true,"priority":10}]'
107
+ export ANGINEER_DOCS_API_URL="https://your-docs-api"
108
+ ```
109
+
110
+ ```python
111
+ from angineer_core.policy_query import run_policy_query
112
+
113
+ result = run_policy_query("隧道二次衬砌的最小厚度是多少?", library_id="default")
114
+
115
+ print(result["answer"]) # 最终回答(含 cite 标记)
116
+ print(result["citations"]) # 出处(来自检索项的 page/cite 字段)
117
+ print(result["intent"]) # 意图分级结果(L0~L4 / service_mode)
118
+ print(result["llm_errors"]) # 空列表=没有静默降级的 LLM 失败
119
+ ```
120
+
121
+ `run_policy_query` 不依赖 HTTP / FastAPI / asyncio,可在后台线程直接调用。
122
+ 返回值是字典:`answer` / `citations` / `retrieved_items` / `evidences` / `intent` /
123
+ `trace_notes` / `stage_timings` / `prompt_versions` / `llm_errors` / `runtime_flags` 等。
124
+
125
+ ## 两条检索接入路径
126
+
127
+ 引擎只认协议,不认识任何具体检索实现;下表的两种接法效果等价,任选其一。
128
+
129
+ | 路径 | 怎么接 | 未接时表现 |
130
+ | :--- | :--- | :--- |
131
+ | **HTTP**(推荐给独立部署) | 配 `ANGINEER_DOCS_API_URL` 指向 docs-api 兼容服务;引擎调 4 个端点:`/api/knowledge/internal/retrieve`、`/internal/entity-search`、`/internal/doc-nodes`、`/internal/graph-append-note` | 未配置则走本地回退(端口路径) |
132
+ | **端口注入**(推荐给嵌入式) | 进程内调 `angineer_core.ports.register_*`,把自家检索实现挂上 | 未注册的端口按降级语义走「警告 + 空结果」,对应工具不可用 |
133
+
134
+ ```python
135
+ from angineer_core import ports
136
+
137
+ ports.register_local_nodes_loader(lambda library_id, doc_ids: my_nodes(library_id, doc_ids))
138
+ ports.register_local_rerank(lambda query, task_type, candidates: my_rerank(query, task_type, candidates))
139
+ ports.register_agent_search(
140
+ normalize_query=my_normalize_query, # "第六十条" → "第60条",不接则原样透传
141
+ knowledge_local=my_knowledge_search, # 只做正文问答时,这个端口是必需的
142
+ table_local=my_table_search,
143
+ entity_local=my_entity_search,
144
+ local_stats=my_local_stats,
145
+ engtool_registry=my_tool_registry, # calculator / conditional 等外部工具
146
+ relevant_citations=my_relevant_citations,
147
+ table_blocks=my_table_blocks,
148
+ )
149
+ ```
150
+
151
+ 只做正文问答(不查表、不查图谱、不做统计)的最小实现 = **两个端口**:
152
+
153
+ - `knowledge_local`:召回本体(必需);
154
+ - `local_nodes_loader`:文档清单(`local_nodes_loader(library_id, doc_ids)`)。它决定证据文本前的
155
+ `《标题》` 前缀,也决定 `doc_ids` 收窄是否生效——**清单为空时按 docs-core 配方的召回恒空**
156
+ (`dense_retriever` 见空 nodes 直接返回 `[]`),表现为整库拒答。自家召回实现若不依赖文档清单,
157
+ 也必须让检索项自带标题(见下条契约)。
158
+
159
+ 表格题 / 实体查 / 统计题 / 引用挑选 / SOP / 计算器各自是独立端口,按需补齐。
160
+
161
+ **检索项契约**(换任何实现都要守,缺字段只会静默退化、不报错):
162
+
163
+ - `item_id` / `doc_id` / `title` / `text` / `score` / `metadata` 字段齐全;
164
+ - **`metadata.doc_title` 必带**(或让 `text` 自带 `《标题》`):引擎据此给证据文本拼 `《书名》` 前缀;
165
+ 出处守卫用「证据里出现过的书名号标题」核对答案里的引用,**证据侧一个标题都没有时,
166
+ 答案里任何 `《…》` 引用都会被判编造、整段替换为拒答**(`has_unsupported_reference`,逐条硬拦会误杀,
167
+ 所以口径是「全部核不到才拦」);
168
+ - 页码等定位字段随 `metadata` 透传,`citations` 由引擎按检索顺序分配标注。
169
+
170
+ ## 意图分级与策略展开
171
+
172
+ `IntentClassifier` 输出 L0~L4,`build_attempts` 按级别展开尝试序列(前一级失败自动降级到下一级):
173
+
174
+ | 级别 | 含义 | Attempt 序列 |
175
+ | :--- | :--- | :--- |
176
+ | L0 | 闲聊 / 寒暄 | 直答(不调工具) |
177
+ | L1 | 正文问答 | 语义检索(`enforce_evidence=True`,无证据拒答) |
178
+ | L2 | 条款 / 表格定位 | `table_search` 优先,无证据降级 L1 |
179
+ | L3 | 规范计算 | 复杂档:QA 三件套 + `sop_execute` + `calculator` + `conditional`,最多 8 轮 |
180
+ | L4 | 复杂综合 | 同 L3(复杂档只按 level 记标签,编排一致) |
181
+
182
+ ## 事件协议
183
+
184
+ `run_agent_loop` 通过 `emit` 回调吐 `AgentEvent(type, run_id, turn, ts, payload)`:
185
+
186
+ ```text
187
+ run_start / run_end
188
+ turn_start / turn_end
189
+ message_start / message_delta / message_end
190
+ tool_start / tool_end
191
+ note(过程说明) / answer(守卫替换后的最终回答) / error
192
+ ```
193
+
194
+ 按 `run_id` 归并以还原「思考过程(N 步 · 工具耗时 X)」;`meta` 字段永不进 LLM 上下文。
195
+
196
+ ## 证据守卫
197
+
198
+ `make_final_answer_guard(enforce_evidence)` 在回答出栈前做四道检查:
199
+
200
+ 1. 全程没有有效证据 → 拒答;
201
+ 2. 答案引用未检索到的规范编号 / 书名号标题 → 替换为拒答话术(标题类为「全部核不到才拦」,部分核到的次级引用放行);
202
+ 3. 编造 `[KTE]` 标记 → 移除标记,不拒答;
203
+ 4. 已有证据却整段拒答 → 终段定向重试一次,仍拒答则保留原文并留 trace 注记。
204
+
205
+ ## 环境变量
206
+
207
+ 配置全部来自环境变量;LLM 侧变量(`LLM_CONFIGS` / `ANGINEER_*` 超时重试等)见
208
+ angineer-ai-inference README,这里只列引擎自身常用的:
209
+
210
+ | 变量 | 默认 | 说明 |
211
+ | :--- | :--- | :--- |
212
+ | `ANGINEER_DOCS_API_URL` | 空 | docs-api 基址;配了就优先走 HTTP 检索 |
213
+ | `ANGINEER_DOCS_API_TIMEOUT` | 30 | docs-api 请求超时(秒) |
214
+ | `ANGINEER_DISABLE_LOCAL_FALLBACK` | 0 | 置 1 时禁止本地回退(HTTP 失败即空结果,不发跨进程直读) |
215
+ | `ANGINEER_CONTEXT_TOP_N` | 15 | rerank 后进上下文的证据条数上限 |
216
+ | `ANGINEER_QA_PROMPT_VERSION` | latest | QA 档提示词版本(`v17` / `latest`,随包发布) |
217
+ | `ANGINEER_FIRST_TOKEN_LIVENESS_S` | 90 | 流式首字存活线(秒),0 = 关闭;防上游挂起不返回 |
218
+ | `ANGINEER_ROUTE_PARALLEL` | true | 赌博式预检:请求进来就并行预跑一次 L1 检索 |
219
+ | `ANGINEER_EAGER_COMPRESS` | false | 激进压缩开关:每轮即压跨对话证据,不等预算阈值 |
220
+ | `ANGINEER_LOG_LEVEL` | INFO | 日志级别 |
221
+
222
+ ## 不在本库范围
223
+
224
+ - HTTP / SSE 服务端点(由消费方,如 AnGIneer 的 aichat-api 实现);
225
+ - 知识库存储、向量检索、rerank、解析入库(由消费方或 docs-api 提供);
226
+ - 会话池与聊天历史持久化(本库给 `AgentSession` 结构,落库由消费方决定);
227
+ - LLM 客户端本身(走 angineer-ai-inference)。
228
+
229
+ ## 开发与测试
230
+
231
+ ```bash
232
+ pip install -e ".[dev]"
233
+ python -m pytest tests -q
234
+ ```
235
+
236
+ 测试覆盖:意图分类降级、策略展开、工具箱组成、答案形态注入、上下文截断、
237
+ 证据指针与跨轮压缩、多库作用域与检索 memo、检索端口双轨。依赖 docs-core 的集成用例
238
+ 在未安装 docs-core 的环境自动跳过(当前 70 passed / 6 skipped)。
239
+
240
+ 架构说明(Agent Harness 详解、工具协议、关键踩坑)见主仓库
241
+ [docs/angineer-core/agent-harness.md](https://github.com/0mao0/AnGIneer/blob/main/docs/angineer-core/agent-harness.md)。
242
+
243
+ ## 许可
244
+
245
+ MIT
@@ -0,0 +1,215 @@
1
+ # angineer-core
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/angineer-core)](https://pypi.org/project/angineer-core/)
4
+
5
+ AnGIneer 的问答编排内核(Agent Harness 引擎,纯 Python 库,非服务):一条
6
+ **意图分级 → 策略展开 → 多轮工具调用 → 证据守卫 → 出处标注** 的完整链路。
7
+
8
+ > 定位:本库**不含 HTTP 服务、不含知识库存储、不含检索配方**。
9
+ > LLM 调用交给 [angineer-ai-inference](https://pypi.org/project/angineer-ai-inference/);
10
+ > 检索实现由消费方注入——要么通过端口注册挂进本进程,要么配 `ANGINEER_DOCS_API_URL` 走 HTTP。
11
+
12
+ ## 功能架构
13
+
14
+ ```mermaid
15
+ flowchart TB
16
+ subgraph entry["入口"]
17
+ pq["run_policy_query(同步、无 HTTP 依赖)"]
18
+ loop["run_agent_loop(自备配置的裸循环)"]
19
+ end
20
+
21
+ subgraph harness["编排"]
22
+ cls["意图分级 IntentClassifier(L0~L4)"]
23
+ pol["策略展开 build_attempts(Attempt 状态机)"]
24
+ guard["证据守卫 make_final_answer_guard"]
25
+ codec["工具文本协议 TextToolCallCodec"]
26
+ end
27
+
28
+ subgraph tools["工具箱"]
29
+ ks["knowledge_search"]
30
+ ts["table_search"]
31
+ es["entity_search"]
32
+ st["knowledge_stats"]
33
+ calc["calculator / sop_execute / conditional"]
34
+ end
35
+
36
+ subgraph seams["检索接入(二选一)"]
37
+ http["HTTP:ANGINEER_DOCS_API_URL(docs-api 契约)"]
38
+ ports["端口注入:ports.register_agent_search 等"]
39
+ end
40
+
41
+ subgraph llm["LLM"]
42
+ ai["angineer-ai-inference(LLM_CONFIGS 多模型路由)"]
43
+ end
44
+
45
+ pq --> cls --> pol --> loop
46
+ loop --> codec
47
+ codec --> tools
48
+ tools --> seams
49
+ loop --> llm
50
+ llm --> guard
51
+ guard --> pq
52
+ ```
53
+
54
+ ## 安装
55
+
56
+ ```bash
57
+ # PyPI(推荐)
58
+ pip install angineer-core
59
+
60
+ # 或从 GitHub 钉版本安装
61
+ pip install "angineer-core @ git+https://github.com/0mao0/angineer-core.git@v0.1.0"
62
+
63
+ # 本地开发(主仓库 AnGIneer 内)
64
+ pip install -e services/angineer-core
65
+ ```
66
+
67
+ Python 要求 `>=3.10`;依赖:`angineer-ai-inference`、`pydantic>=2.0,<3.0`、
68
+ `python-dotenv>=1.0`、`requests>=2.31`、`jsonschema`。
69
+
70
+ ## 快速开始
71
+
72
+ 两步就位:配 LLM(`LLM_CONFIGS`,格式见 [angineer-ai-inference](https://pypi.org/project/angineer-ai-inference/)),
73
+ 再指一条检索来源——下面是走 HTTP 的最小例子。
74
+
75
+ ```bash
76
+ export LLM_CONFIGS='[{"name":"Qwen3.6-A3B","model":"Qwen3.6-35B-A3B-FP8","api_key":"sk-...","base_url":"https://your-gateway/v1","enabled":true,"priority":10}]'
77
+ export ANGINEER_DOCS_API_URL="https://your-docs-api"
78
+ ```
79
+
80
+ ```python
81
+ from angineer_core.policy_query import run_policy_query
82
+
83
+ result = run_policy_query("隧道二次衬砌的最小厚度是多少?", library_id="default")
84
+
85
+ print(result["answer"]) # 最终回答(含 cite 标记)
86
+ print(result["citations"]) # 出处(来自检索项的 page/cite 字段)
87
+ print(result["intent"]) # 意图分级结果(L0~L4 / service_mode)
88
+ print(result["llm_errors"]) # 空列表=没有静默降级的 LLM 失败
89
+ ```
90
+
91
+ `run_policy_query` 不依赖 HTTP / FastAPI / asyncio,可在后台线程直接调用。
92
+ 返回值是字典:`answer` / `citations` / `retrieved_items` / `evidences` / `intent` /
93
+ `trace_notes` / `stage_timings` / `prompt_versions` / `llm_errors` / `runtime_flags` 等。
94
+
95
+ ## 两条检索接入路径
96
+
97
+ 引擎只认协议,不认识任何具体检索实现;下表的两种接法效果等价,任选其一。
98
+
99
+ | 路径 | 怎么接 | 未接时表现 |
100
+ | :--- | :--- | :--- |
101
+ | **HTTP**(推荐给独立部署) | 配 `ANGINEER_DOCS_API_URL` 指向 docs-api 兼容服务;引擎调 4 个端点:`/api/knowledge/internal/retrieve`、`/internal/entity-search`、`/internal/doc-nodes`、`/internal/graph-append-note` | 未配置则走本地回退(端口路径) |
102
+ | **端口注入**(推荐给嵌入式) | 进程内调 `angineer_core.ports.register_*`,把自家检索实现挂上 | 未注册的端口按降级语义走「警告 + 空结果」,对应工具不可用 |
103
+
104
+ ```python
105
+ from angineer_core import ports
106
+
107
+ ports.register_local_nodes_loader(lambda library_id, doc_ids: my_nodes(library_id, doc_ids))
108
+ ports.register_local_rerank(lambda query, task_type, candidates: my_rerank(query, task_type, candidates))
109
+ ports.register_agent_search(
110
+ normalize_query=my_normalize_query, # "第六十条" → "第60条",不接则原样透传
111
+ knowledge_local=my_knowledge_search, # 只做正文问答时,这个端口是必需的
112
+ table_local=my_table_search,
113
+ entity_local=my_entity_search,
114
+ local_stats=my_local_stats,
115
+ engtool_registry=my_tool_registry, # calculator / conditional 等外部工具
116
+ relevant_citations=my_relevant_citations,
117
+ table_blocks=my_table_blocks,
118
+ )
119
+ ```
120
+
121
+ 只做正文问答(不查表、不查图谱、不做统计)的最小实现 = **两个端口**:
122
+
123
+ - `knowledge_local`:召回本体(必需);
124
+ - `local_nodes_loader`:文档清单(`local_nodes_loader(library_id, doc_ids)`)。它决定证据文本前的
125
+ `《标题》` 前缀,也决定 `doc_ids` 收窄是否生效——**清单为空时按 docs-core 配方的召回恒空**
126
+ (`dense_retriever` 见空 nodes 直接返回 `[]`),表现为整库拒答。自家召回实现若不依赖文档清单,
127
+ 也必须让检索项自带标题(见下条契约)。
128
+
129
+ 表格题 / 实体查 / 统计题 / 引用挑选 / SOP / 计算器各自是独立端口,按需补齐。
130
+
131
+ **检索项契约**(换任何实现都要守,缺字段只会静默退化、不报错):
132
+
133
+ - `item_id` / `doc_id` / `title` / `text` / `score` / `metadata` 字段齐全;
134
+ - **`metadata.doc_title` 必带**(或让 `text` 自带 `《标题》`):引擎据此给证据文本拼 `《书名》` 前缀;
135
+ 出处守卫用「证据里出现过的书名号标题」核对答案里的引用,**证据侧一个标题都没有时,
136
+ 答案里任何 `《…》` 引用都会被判编造、整段替换为拒答**(`has_unsupported_reference`,逐条硬拦会误杀,
137
+ 所以口径是「全部核不到才拦」);
138
+ - 页码等定位字段随 `metadata` 透传,`citations` 由引擎按检索顺序分配标注。
139
+
140
+ ## 意图分级与策略展开
141
+
142
+ `IntentClassifier` 输出 L0~L4,`build_attempts` 按级别展开尝试序列(前一级失败自动降级到下一级):
143
+
144
+ | 级别 | 含义 | Attempt 序列 |
145
+ | :--- | :--- | :--- |
146
+ | L0 | 闲聊 / 寒暄 | 直答(不调工具) |
147
+ | L1 | 正文问答 | 语义检索(`enforce_evidence=True`,无证据拒答) |
148
+ | L2 | 条款 / 表格定位 | `table_search` 优先,无证据降级 L1 |
149
+ | L3 | 规范计算 | 复杂档:QA 三件套 + `sop_execute` + `calculator` + `conditional`,最多 8 轮 |
150
+ | L4 | 复杂综合 | 同 L3(复杂档只按 level 记标签,编排一致) |
151
+
152
+ ## 事件协议
153
+
154
+ `run_agent_loop` 通过 `emit` 回调吐 `AgentEvent(type, run_id, turn, ts, payload)`:
155
+
156
+ ```text
157
+ run_start / run_end
158
+ turn_start / turn_end
159
+ message_start / message_delta / message_end
160
+ tool_start / tool_end
161
+ note(过程说明) / answer(守卫替换后的最终回答) / error
162
+ ```
163
+
164
+ 按 `run_id` 归并以还原「思考过程(N 步 · 工具耗时 X)」;`meta` 字段永不进 LLM 上下文。
165
+
166
+ ## 证据守卫
167
+
168
+ `make_final_answer_guard(enforce_evidence)` 在回答出栈前做四道检查:
169
+
170
+ 1. 全程没有有效证据 → 拒答;
171
+ 2. 答案引用未检索到的规范编号 / 书名号标题 → 替换为拒答话术(标题类为「全部核不到才拦」,部分核到的次级引用放行);
172
+ 3. 编造 `[KTE]` 标记 → 移除标记,不拒答;
173
+ 4. 已有证据却整段拒答 → 终段定向重试一次,仍拒答则保留原文并留 trace 注记。
174
+
175
+ ## 环境变量
176
+
177
+ 配置全部来自环境变量;LLM 侧变量(`LLM_CONFIGS` / `ANGINEER_*` 超时重试等)见
178
+ angineer-ai-inference README,这里只列引擎自身常用的:
179
+
180
+ | 变量 | 默认 | 说明 |
181
+ | :--- | :--- | :--- |
182
+ | `ANGINEER_DOCS_API_URL` | 空 | docs-api 基址;配了就优先走 HTTP 检索 |
183
+ | `ANGINEER_DOCS_API_TIMEOUT` | 30 | docs-api 请求超时(秒) |
184
+ | `ANGINEER_DISABLE_LOCAL_FALLBACK` | 0 | 置 1 时禁止本地回退(HTTP 失败即空结果,不发跨进程直读) |
185
+ | `ANGINEER_CONTEXT_TOP_N` | 15 | rerank 后进上下文的证据条数上限 |
186
+ | `ANGINEER_QA_PROMPT_VERSION` | latest | QA 档提示词版本(`v17` / `latest`,随包发布) |
187
+ | `ANGINEER_FIRST_TOKEN_LIVENESS_S` | 90 | 流式首字存活线(秒),0 = 关闭;防上游挂起不返回 |
188
+ | `ANGINEER_ROUTE_PARALLEL` | true | 赌博式预检:请求进来就并行预跑一次 L1 检索 |
189
+ | `ANGINEER_EAGER_COMPRESS` | false | 激进压缩开关:每轮即压跨对话证据,不等预算阈值 |
190
+ | `ANGINEER_LOG_LEVEL` | INFO | 日志级别 |
191
+
192
+ ## 不在本库范围
193
+
194
+ - HTTP / SSE 服务端点(由消费方,如 AnGIneer 的 aichat-api 实现);
195
+ - 知识库存储、向量检索、rerank、解析入库(由消费方或 docs-api 提供);
196
+ - 会话池与聊天历史持久化(本库给 `AgentSession` 结构,落库由消费方决定);
197
+ - LLM 客户端本身(走 angineer-ai-inference)。
198
+
199
+ ## 开发与测试
200
+
201
+ ```bash
202
+ pip install -e ".[dev]"
203
+ python -m pytest tests -q
204
+ ```
205
+
206
+ 测试覆盖:意图分类降级、策略展开、工具箱组成、答案形态注入、上下文截断、
207
+ 证据指针与跨轮压缩、多库作用域与检索 memo、检索端口双轨。依赖 docs-core 的集成用例
208
+ 在未安装 docs-core 的环境自动跳过(当前 70 passed / 6 skipped)。
209
+
210
+ 架构说明(Agent Harness 详解、工具协议、关键踩坑)见主仓库
211
+ [docs/angineer-core/agent-harness.md](https://github.com/0mao0/AnGIneer/blob/main/docs/angineer-core/agent-harness.md)。
212
+
213
+ ## 许可
214
+
215
+ MIT
@@ -0,0 +1,60 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "angineer-core"
7
+ version = "0.1.0"
8
+ description = "AnGIneer core service - AI Engineer Operating System Core Module"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "AnGIneer Team"}
14
+ ]
15
+ keywords = ["ai", "agent", "sop", "engineering"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ ]
25
+ dependencies = [
26
+ "angineer-ai-inference>=0.2.2,<0.3.0",
27
+ "jsonschema>=4.0.0",
28
+ "pydantic>=2.0.0,<3.0.0",
29
+ "python-dotenv>=1.0.0",
30
+ "requests>=2.31.0",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ dev = [
35
+ "pytest>=7.0.0",
36
+ "pytest-cov>=4.0.0",
37
+ "mypy>=1.0.0",
38
+ ]
39
+
40
+ [tool.setuptools]
41
+ package-dir = {"" = "src"}
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["src"]
45
+
46
+ [project.urls]
47
+ Repository = "https://github.com/0mao0/angineer-core"
48
+ Issues = "https://github.com/0mao0/angineer-core/issues"
49
+
50
+ [tool.mypy]
51
+ python_version = "3.10"
52
+ warn_return_any = true
53
+ warn_unused_configs = true
54
+ ignore_missing_imports = true
55
+
56
+ [tool.pytest.ini_options]
57
+ testpaths = ["tests"]
58
+ python_files = ["test_*.py"]
59
+ python_classes = ["Test*"]
60
+ python_functions = ["test_*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,91 @@
1
+ """
2
+ AnGIneer Core - AI Engineer Operating System Core Module.
3
+
4
+ LLM 相关功能请直接使用 ai_inference:
5
+ from ai_inference.llm_client import LLMClient, get_llm_client
6
+ from ai_inference.llm_response_parser import ParseError, extract_json_from_text
7
+
8
+ 惰性导出(PEP 562,2026-09-18):本包 __init__ 不再 import classifier/memory/
9
+ base_config/base_di 等重组件——此前 `import angineer_core.agent_messages` 也会
10
+ 顺带拉起 ai_inference/docs-core 整条依赖树(importtime 实测 520ms+),独立包
11
+ (如 chat-history)只想用轻量数据类/协议却被迫背全家桶。现改为 __getattr__
12
+ 按需加载并缓存到 globals(),`from angineer_core import IntentClassifier` 等
13
+ 既有用法逐字兼容。
14
+ """
15
+
16
+ import importlib
17
+ from typing import TYPE_CHECKING
18
+
19
+ if TYPE_CHECKING: # 仅为类型检查器保留的静态视图,运行时不执行任何重导入
20
+ from angineer_core.classifier import IntentClassifier
21
+ from angineer_core.memory import Memory, StepRecord, UndefinedVariableError
22
+ from angineer_core.base_contracts import (
23
+ SOP, Step, AgentResponse,
24
+ IntentResult, IntentLevel, ServiceMode,
25
+ IntentResponse, ActionResponse, StepParseResponse, ArgsExtractResponse,
26
+ ScopeContext, RouteDebug, RouteDecision, Evidence, EvidenceKind,
27
+ )
28
+ from angineer_core.base_logger import (
29
+ get_logger,
30
+ get_default_logger,
31
+ set_default_logger,
32
+ )
33
+ from angineer_core.base_config import (
34
+ AnGIneerConfig,
35
+ get_config,
36
+ set_config,
37
+ reset_config,
38
+ )
39
+ from angineer_core.base_di import (
40
+ setup_container,
41
+ initialize_services,
42
+ )
43
+
44
+ __version__ = "0.1.0"
45
+
46
+ # 导出名 → 所属子模块:__getattr__ 命中时才 importlib.import_module
47
+ _LAZY_EXPORTS = {
48
+ "IntentClassifier": "classifier",
49
+ "Memory": "memory",
50
+ "StepRecord": "memory",
51
+ "UndefinedVariableError": "memory",
52
+ "SOP": "base_contracts",
53
+ "Step": "base_contracts",
54
+ "AgentResponse": "base_contracts",
55
+ "IntentResult": "base_contracts",
56
+ "IntentLevel": "base_contracts",
57
+ "ServiceMode": "base_contracts",
58
+ "IntentResponse": "base_contracts",
59
+ "ActionResponse": "base_contracts",
60
+ "StepParseResponse": "base_contracts",
61
+ "ArgsExtractResponse": "base_contracts",
62
+ "ScopeContext": "base_contracts",
63
+ "RouteDebug": "base_contracts",
64
+ "RouteDecision": "base_contracts",
65
+ "Evidence": "base_contracts",
66
+ "EvidenceKind": "base_contracts",
67
+ "get_logger": "base_logger",
68
+ "get_default_logger": "base_logger",
69
+ "set_default_logger": "base_logger",
70
+ "AnGIneerConfig": "base_config",
71
+ "get_config": "base_config",
72
+ "set_config": "base_config",
73
+ "reset_config": "base_config",
74
+ "setup_container": "base_di",
75
+ "initialize_services": "base_di",
76
+ }
77
+
78
+ __all__ = sorted(_LAZY_EXPORTS)
79
+
80
+
81
+ def __getattr__(name: str):
82
+ mod_name = _LAZY_EXPORTS.get(name)
83
+ if mod_name is None:
84
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
85
+ value = getattr(importlib.import_module(f"{__name__}.{mod_name}"), name)
86
+ globals()[name] = value # 缓存:后续访问不再走 __getattr__
87
+ return value
88
+
89
+
90
+ def __dir__():
91
+ return sorted(set(globals()) | set(_LAZY_EXPORTS))