loom-agent 0.0.1__py3-none-any.whl → 0.0.3__py3-none-any.whl

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.

Potentially problematic release.


This version of loom-agent might be problematic. Click here for more details.

Files changed (39) hide show
  1. loom/builtin/tools/calculator.py +4 -0
  2. loom/builtin/tools/document_search.py +5 -0
  3. loom/builtin/tools/glob.py +4 -0
  4. loom/builtin/tools/grep.py +4 -0
  5. loom/builtin/tools/http_request.py +5 -0
  6. loom/builtin/tools/python_repl.py +5 -0
  7. loom/builtin/tools/read_file.py +4 -0
  8. loom/builtin/tools/task.py +105 -0
  9. loom/builtin/tools/web_search.py +4 -0
  10. loom/builtin/tools/write_file.py +4 -0
  11. loom/components/agent.py +121 -5
  12. loom/core/agent_executor.py +777 -321
  13. loom/core/compression_manager.py +17 -10
  14. loom/core/context_assembly.py +437 -0
  15. loom/core/events.py +660 -0
  16. loom/core/execution_context.py +119 -0
  17. loom/core/tool_orchestrator.py +383 -0
  18. loom/core/turn_state.py +188 -0
  19. loom/core/types.py +15 -4
  20. loom/core/unified_coordination.py +389 -0
  21. loom/interfaces/event_producer.py +172 -0
  22. loom/interfaces/tool.py +22 -1
  23. loom/security/__init__.py +13 -0
  24. loom/security/models.py +85 -0
  25. loom/security/path_validator.py +128 -0
  26. loom/security/validator.py +346 -0
  27. loom/tasks/PHASE_1_FOUNDATION/task_1.1_agent_events.md +121 -0
  28. loom/tasks/PHASE_1_FOUNDATION/task_1.2_streaming_api.md +521 -0
  29. loom/tasks/PHASE_1_FOUNDATION/task_1.3_context_assembler.md +606 -0
  30. loom/tasks/PHASE_2_CORE_FEATURES/task_2.1_tool_orchestrator.md +743 -0
  31. loom/tasks/PHASE_2_CORE_FEATURES/task_2.2_security_validator.md +676 -0
  32. loom/tasks/README.md +109 -0
  33. loom/tasks/__init__.py +11 -0
  34. loom/tasks/sql_placeholder.py +100 -0
  35. loom_agent-0.0.3.dist-info/METADATA +292 -0
  36. {loom_agent-0.0.1.dist-info → loom_agent-0.0.3.dist-info}/RECORD +38 -19
  37. loom_agent-0.0.1.dist-info/METADATA +0 -457
  38. {loom_agent-0.0.1.dist-info → loom_agent-0.0.3.dist-info}/WHEEL +0 -0
  39. {loom_agent-0.0.1.dist-info → loom_agent-0.0.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,606 @@
1
+ # Task 1.3: 修复 RAG Context Bug - 创建 ContextAssembler
2
+
3
+ **状态**: ⏳ 待开始
4
+ **优先级**: P0
5
+ **预计时间**: 1-2 天
6
+ **依赖**: Task 1.2 (流式 API) ✅
7
+
8
+ ---
9
+
10
+ ## 📋 任务概述
11
+
12
+ ### 目标
13
+
14
+ 创建 `ContextAssembler` 组件,修复 RAG 上下文被系统提示覆盖的 Bug,实现基于优先级的智能上下文组装。
15
+
16
+ ### 为什么需要这个任务?
17
+
18
+ **当前问题 (Loom 1.0)**:
19
+ ```python
20
+ # loom/core/agent_executor.py:664-671
21
+ def _inject_system_prompt(self, history: List[Message], system_prompt: str) -> List[Message]:
22
+ """注入或更新系统提示消息"""
23
+ # 如果第一条是系统消息,则替换;否则在开头插入
24
+ if history and history[0].role == "system":
25
+ history[0] = Message(role="system", content=system_prompt) # ❌ 覆盖了 RAG 上下文!
26
+ else:
27
+ history.insert(0, Message(role="system", content=system_prompt))
28
+ return history
29
+ ```
30
+
31
+ **问题**:
32
+ 1. RAG 检索的文档上下文被注入到 `history` 中作为 system 消息
33
+ 2. `_inject_system_prompt` 直接覆盖第一个 system 消息
34
+ 3. 导致 RAG 上下文丢失,LLM 无法看到检索的文档
35
+
36
+ **期望结果 (Loom 2.0)**:
37
+ ```python
38
+ # 使用 ContextAssembler 智能组装
39
+ assembler = ContextAssembler(max_tokens=4000)
40
+ assembler.add_component("base_instructions", base_prompt, priority=100)
41
+ assembler.add_component("tool_schema", tool_definitions, priority=80)
42
+ assembler.add_component("retrieved_docs", rag_context, priority=90) # 高优先级,不被覆盖
43
+ assembler.add_component("examples", few_shot_examples, priority=50)
44
+
45
+ final_system_prompt = assembler.assemble() # 智能合并,保证 RAG 上下文存在
46
+ ```
47
+
48
+ ---
49
+
50
+ ## 📐 详细步骤
51
+
52
+ ### Step 1: 创建 ContextAssembler
53
+
54
+ **文件**: `loom/core/context_assembly.py` (新建)
55
+
56
+ **核心类设计**:
57
+ ```python
58
+ from dataclasses import dataclass
59
+ from typing import Dict, List, Optional
60
+ from enum import IntEnum
61
+
62
+
63
+ class ComponentPriority(IntEnum):
64
+ """组件优先级枚举"""
65
+ CRITICAL = 100 # 基础指令(必须包含)
66
+ HIGH = 90 # RAG 上下文、重要配置
67
+ MEDIUM = 70 # 工具定义
68
+ LOW = 50 # 示例、额外提示
69
+ OPTIONAL = 30 # 可选内容
70
+
71
+
72
+ @dataclass
73
+ class ContextComponent:
74
+ """上下文组件"""
75
+ name: str
76
+ content: str
77
+ priority: int
78
+ token_count: int
79
+ truncatable: bool = True
80
+
81
+
82
+ class ContextAssembler:
83
+ """
84
+ 智能上下文组装器。
85
+
86
+ 功能:
87
+ - 按优先级组装多个组件
88
+ - Token 预算管理
89
+ - 智能截断低优先级组件
90
+ - 保证高优先级组件完整性
91
+ """
92
+
93
+ def __init__(
94
+ self,
95
+ max_tokens: int = 16000,
96
+ token_counter: Optional[callable] = None
97
+ ):
98
+ """
99
+ Args:
100
+ max_tokens: 最大 token 预算
101
+ token_counter: Token 计数函数(默认使用简单估算)
102
+ """
103
+ self.max_tokens = max_tokens
104
+ self.token_counter = token_counter or self._estimate_tokens
105
+ self.components: List[ContextComponent] = []
106
+
107
+ def add_component(
108
+ self,
109
+ name: str,
110
+ content: str,
111
+ priority: int,
112
+ truncatable: bool = True
113
+ ):
114
+ """
115
+ 添加上下文组件。
116
+
117
+ Args:
118
+ name: 组件名称(如 "base_instructions", "retrieved_docs")
119
+ content: 组件内容
120
+ priority: 优先级(0-100,越高越重要)
121
+ truncatable: 是否可截断
122
+ """
123
+ if not content:
124
+ return
125
+
126
+ token_count = self.token_counter(content)
127
+ component = ContextComponent(
128
+ name=name,
129
+ content=content,
130
+ priority=priority,
131
+ token_count=token_count,
132
+ truncatable=truncatable
133
+ )
134
+ self.components.append(component)
135
+
136
+ def assemble(self) -> str:
137
+ """
138
+ 组装最终上下文。
139
+
140
+ 策略:
141
+ 1. 按优先级降序排序
142
+ 2. 依次添加组件直到超出预算
143
+ 3. 对可截断组件进行智能截断
144
+ 4. 合并所有组件
145
+
146
+ Returns:
147
+ 组装后的上下文字符串
148
+ """
149
+ # 按优先级排序
150
+ sorted_components = sorted(
151
+ self.components,
152
+ key=lambda c: c.priority,
153
+ reverse=True
154
+ )
155
+
156
+ # 计算当前总 Token
157
+ total_tokens = sum(c.token_count for c in sorted_components)
158
+
159
+ # 如果超出预算,进行截断
160
+ if total_tokens > self.max_tokens:
161
+ sorted_components = self._truncate_components(sorted_components)
162
+
163
+ # 合并组件
164
+ sections = []
165
+ for component in sorted_components:
166
+ sections.append(f"# {component.name.upper()}\n{component.content}")
167
+
168
+ return "\n\n".join(sections)
169
+
170
+ def _truncate_components(
171
+ self,
172
+ components: List[ContextComponent]
173
+ ) -> List[ContextComponent]:
174
+ """
175
+ 智能截断组件以满足 token 预算。
176
+
177
+ 策略:
178
+ 1. 保留所有 truncatable=False 的组件
179
+ 2. 按优先级降序处理可截断组件
180
+ 3. 为每个组件分配 token 配额
181
+ """
182
+ budget_remaining = self.max_tokens
183
+ result = []
184
+
185
+ # 首先添加所有不可截断组件
186
+ for comp in components:
187
+ if not comp.truncatable:
188
+ if comp.token_count <= budget_remaining:
189
+ result.append(comp)
190
+ budget_remaining -= comp.token_count
191
+ else:
192
+ # 不可截断但超出预算,跳过(或抛出警告)
193
+ print(f"Warning: Component '{comp.name}' is too large and cannot be truncated")
194
+
195
+ # 然后添加可截断组件
196
+ truncatable = [c for c in components if c.truncatable]
197
+
198
+ for comp in truncatable:
199
+ if comp.token_count <= budget_remaining:
200
+ # 完整添加
201
+ result.append(comp)
202
+ budget_remaining -= comp.token_count
203
+ elif budget_remaining > 100: # 至少保留 100 tokens
204
+ # 截断添加
205
+ truncated_content = self._truncate_content(
206
+ comp.content,
207
+ budget_remaining
208
+ )
209
+ truncated_comp = ContextComponent(
210
+ name=comp.name,
211
+ content=truncated_content,
212
+ priority=comp.priority,
213
+ token_count=budget_remaining,
214
+ truncatable=comp.truncatable
215
+ )
216
+ result.append(truncated_comp)
217
+ budget_remaining = 0
218
+ break
219
+
220
+ return result
221
+
222
+ def _truncate_content(self, content: str, max_tokens: int) -> str:
223
+ """
224
+ 截断内容以适应 token 限制。
225
+
226
+ 策略: 简单按字符比例截断
227
+ """
228
+ ratio = max_tokens / self.token_counter(content)
229
+ target_chars = int(len(content) * ratio * 0.95) # 保守估计
230
+
231
+ if target_chars < len(content):
232
+ return content[:target_chars] + "\n... (truncated)"
233
+ return content
234
+
235
+ def _estimate_tokens(self, text: str) -> int:
236
+ """
237
+ 简单估算 token 数量。
238
+
239
+ 粗略估算: 1 token ≈ 4 字符(英文)
240
+ """
241
+ return len(text) // 4
242
+
243
+ def get_summary(self) -> Dict:
244
+ """返回组装摘要(用于调试)"""
245
+ total_tokens = sum(c.token_count for c in self.components)
246
+ return {
247
+ "components": [
248
+ {
249
+ "name": c.name,
250
+ "priority": c.priority,
251
+ "tokens": c.token_count,
252
+ "truncatable": c.truncatable
253
+ }
254
+ for c in sorted(self.components, key=lambda x: x.priority, reverse=True)
255
+ ],
256
+ "total_tokens": total_tokens,
257
+ "budget": self.max_tokens,
258
+ "overflow": total_tokens - self.max_tokens if total_tokens > self.max_tokens else 0
259
+ }
260
+ ```
261
+
262
+ ---
263
+
264
+ ### Step 2: 修改 AgentExecutor
265
+
266
+ **文件**: `loom/core/agent_executor.py`
267
+
268
+ **修改点**:
269
+
270
+ 1. **导入 ContextAssembler**:
271
+ ```python
272
+ from loom.core.context_assembly import ContextAssembler, ComponentPriority
273
+ ```
274
+
275
+ 2. **删除 `_inject_system_prompt` 方法**:
276
+ ```python
277
+ # 删除这个方法(第 664-671 行)
278
+ def _inject_system_prompt(self, history: List[Message], system_prompt: str) -> List[Message]:
279
+ ...
280
+ ```
281
+
282
+ 3. **修改 `execute_stream()` 中的上下文组装逻辑**:
283
+ ```python
284
+ # 在 execute_stream() 中 (约第 440 行)
285
+
286
+ # Step 4: 使用 ContextAssembler 组装系统提示
287
+ assembler = ContextAssembler(max_tokens=self.max_context_tokens)
288
+
289
+ # 添加基础指令
290
+ base_instructions = self.system_instructions or ""
291
+ if base_instructions:
292
+ assembler.add_component(
293
+ name="base_instructions",
294
+ content=base_instructions,
295
+ priority=ComponentPriority.CRITICAL,
296
+ truncatable=False
297
+ )
298
+
299
+ # 添加 RAG 上下文(如果有)
300
+ if retrieved_docs:
301
+ doc_context = self.context_retriever.format_documents(retrieved_docs)
302
+ assembler.add_component(
303
+ name="retrieved_context",
304
+ content=doc_context,
305
+ priority=ComponentPriority.HIGH,
306
+ truncatable=True
307
+ )
308
+
309
+ # 添加工具定义
310
+ if self.tools:
311
+ tools_spec = self._serialize_tools()
312
+ tools_prompt = build_tools_prompt(tools_spec)
313
+ assembler.add_component(
314
+ name="tool_definitions",
315
+ content=tools_prompt,
316
+ priority=ComponentPriority.MEDIUM,
317
+ truncatable=False
318
+ )
319
+
320
+ # 组装最终系统提示
321
+ final_system_prompt = assembler.assemble()
322
+
323
+ # 注入到 history
324
+ if history and history[0].role == "system":
325
+ history[0] = Message(role="system", content=final_system_prompt)
326
+ else:
327
+ history.insert(0, Message(role="system", content=final_system_prompt))
328
+ ```
329
+
330
+ 4. **添加调试事件**:
331
+ ```python
332
+ # 在组装后发出事件
333
+ summary = assembler.get_summary()
334
+ yield AgentEvent(
335
+ type=AgentEventType.CONTEXT_ASSEMBLED,
336
+ metadata={
337
+ "total_tokens": summary["total_tokens"],
338
+ "components": len(summary["components"]),
339
+ "overflow": summary["overflow"]
340
+ }
341
+ )
342
+ ```
343
+
344
+ ---
345
+
346
+ ### Step 3: 添加新的事件类型(可选)
347
+
348
+ **文件**: `loom/core/events.py`
349
+
350
+ 如果需要更详细的上下文组装可观测性,可以添加:
351
+
352
+ ```python
353
+ class AgentEventType(Enum):
354
+ # ... 现有事件 ...
355
+
356
+ # Context Assembly Events (可选)
357
+ CONTEXT_ASSEMBLED = "context_assembled"
358
+ ```
359
+
360
+ ---
361
+
362
+ ## 🧪 测试要求
363
+
364
+ ### 单元测试
365
+
366
+ **文件**: `tests/unit/test_context_assembler.py` (新建)
367
+
368
+ 测试用例:
369
+
370
+ 1. **基本组装**
371
+ ```python
372
+ def test_basic_assembly():
373
+ """测试基本组装功能"""
374
+ assembler = ContextAssembler(max_tokens=1000)
375
+ assembler.add_component("part1", "Hello", priority=100)
376
+ assembler.add_component("part2", "World", priority=90)
377
+
378
+ result = assembler.assemble()
379
+
380
+ assert "part1" in result.upper()
381
+ assert "Hello" in result
382
+ assert "World" in result
383
+ ```
384
+
385
+ 2. **优先级排序**
386
+ ```python
387
+ def test_priority_ordering():
388
+ """测试优先级排序"""
389
+ assembler = ContextAssembler(max_tokens=10000)
390
+ assembler.add_component("low", "Low priority", priority=10)
391
+ assembler.add_component("high", "High priority", priority=100)
392
+ assembler.add_component("mid", "Mid priority", priority=50)
393
+
394
+ result = assembler.assemble()
395
+
396
+ # 高优先级应该在前
397
+ high_pos = result.find("High priority")
398
+ mid_pos = result.find("Mid priority")
399
+ low_pos = result.find("Low priority")
400
+
401
+ assert high_pos < mid_pos < low_pos
402
+ ```
403
+
404
+ 3. **Token 预算管理**
405
+ ```python
406
+ def test_token_budget():
407
+ """测试 token 预算限制"""
408
+ assembler = ContextAssembler(max_tokens=100)
409
+
410
+ # 添加超出预算的内容
411
+ assembler.add_component("large", "x" * 1000, priority=50, truncatable=True)
412
+ assembler.add_component("critical", "Important", priority=100, truncatable=False)
413
+
414
+ result = assembler.assemble()
415
+ summary = assembler.get_summary()
416
+
417
+ # 关键内容应该保留
418
+ assert "Important" in result
419
+ # 总 token 应该在预算内
420
+ assert summary["total_tokens"] <= 100
421
+ ```
422
+
423
+ 4. **不可截断组件**
424
+ ```python
425
+ def test_non_truncatable_components():
426
+ """测试不可截断组件保护"""
427
+ assembler = ContextAssembler(max_tokens=500)
428
+
429
+ assembler.add_component("critical", "Critical content", priority=100, truncatable=False)
430
+ assembler.add_component("optional", "x" * 10000, priority=50, truncatable=True)
431
+
432
+ result = assembler.assemble()
433
+
434
+ # 关键内容必须完整
435
+ assert "Critical content" in result
436
+ assert result.count("Critical content") == 1 # 没有被截断
437
+ ```
438
+
439
+ 5. **RAG 上下文保护**
440
+ ```python
441
+ def test_rag_context_preserved():
442
+ """测试 RAG 上下文被正确保留"""
443
+ assembler = ContextAssembler(max_tokens=2000)
444
+
445
+ assembler.add_component(
446
+ "base_instructions",
447
+ "You are a helpful assistant.",
448
+ priority=ComponentPriority.CRITICAL,
449
+ truncatable=False
450
+ )
451
+
452
+ assembler.add_component(
453
+ "retrieved_docs",
454
+ "Document 1: Important info\nDocument 2: More info",
455
+ priority=ComponentPriority.HIGH,
456
+ truncatable=True
457
+ )
458
+
459
+ result = assembler.assemble()
460
+
461
+ # RAG 上下文应该存在
462
+ assert "Document 1" in result
463
+ assert "Important info" in result
464
+ ```
465
+
466
+ ### 集成测试
467
+
468
+ **文件**: `tests/integration/test_rag_context_fix.py` (新建)
469
+
470
+ 测试真实场景:
471
+
472
+ ```python
473
+ @pytest.mark.asyncio
474
+ async def test_rag_context_not_overwritten():
475
+ """测试 RAG 上下文不被系统提示覆盖"""
476
+ # 创建带 RAG 的 agent
477
+ from loom.rag import MockRetriever
478
+
479
+ retriever = MockRetriever(docs=[
480
+ {"content": "Python is a programming language", "score": 0.9}
481
+ ])
482
+
483
+ agent = Agent(
484
+ llm=mock_llm,
485
+ context_retriever=retriever,
486
+ system_instructions="You are helpful"
487
+ )
488
+
489
+ collector = EventCollector()
490
+ async for event in agent.execute("What is Python?"):
491
+ collector.add(event)
492
+
493
+ # 验证 RAG 上下文在 LLM 调用中存在
494
+ # (需要检查发送给 LLM 的消息)
495
+ # 这个测试可能需要 mock LLM 来验证
496
+ ```
497
+
498
+ ---
499
+
500
+ ## ✅ 验收标准
501
+
502
+ | 标准 | 要求 | 检查 |
503
+ |------|------|------|
504
+ | ContextAssembler 实现 | 完整功能 | [ ] |
505
+ | 优先级排序 | 正确排序组件 | [ ] |
506
+ | Token 预算管理 | 不超出限制 | [ ] |
507
+ | RAG 上下文保留 | 不被覆盖 | [ ] |
508
+ | 删除旧方法 | `_inject_system_prompt` 已删除 | [ ] |
509
+ | 测试覆盖率 | ≥ 80% | [ ] |
510
+ | 所有测试通过 | 单元 + 集成测试 | [ ] |
511
+ | 向后兼容 | 不破坏现有功能 | [ ] |
512
+
513
+ ---
514
+
515
+ ## 📋 实施检查清单
516
+
517
+ ### 代码实现
518
+ - [ ] 创建 `loom/core/context_assembly.py`
519
+ - [ ] `ComponentPriority` 枚举
520
+ - [ ] `ContextComponent` 数据类
521
+ - [ ] `ContextAssembler` 类
522
+ - [ ] `add_component()` 方法
523
+ - [ ] `assemble()` 方法
524
+ - [ ] `_truncate_components()` 方法
525
+ - [ ] `get_summary()` 方法
526
+
527
+ - [ ] 修改 `loom/core/agent_executor.py`
528
+ - [ ] 导入 `ContextAssembler`
529
+ - [ ] 删除 `_inject_system_prompt()` 方法
530
+ - [ ] 修改 `execute_stream()` 使用 ContextAssembler
531
+ - [ ] 添加 CONTEXT_ASSEMBLED 事件(可选)
532
+
533
+ ### 测试
534
+ - [ ] 创建 `tests/unit/test_context_assembler.py`
535
+ - [ ] 5+ 单元测试
536
+ - [ ] 覆盖所有核心功能
537
+
538
+ - [ ] 创建 `tests/integration/test_rag_context_fix.py`
539
+ - [ ] End-to-end RAG 测试
540
+ - [ ] 验证上下文不被覆盖
541
+
542
+ - [ ] 运行所有测试
543
+ ```bash
544
+ pytest tests/unit/test_context_assembler.py -v
545
+ pytest tests/integration/test_rag_context_fix.py -v
546
+ pytest tests/ -v # 确保没有破坏现有功能
547
+ ```
548
+
549
+ ### 文档
550
+ - [ ] 创建 `examples/rag_context_example.py`
551
+ - [ ] 演示 RAG 集成
552
+ - [ ] 展示上下文组装
553
+
554
+ - [ ] 更新 `docs/api_reference.md`
555
+ - [ ] 添加 ContextAssembler API 文档
556
+
557
+ - [ ] 创建 `docs/TASK_1.3_COMPLETION_SUMMARY.md`
558
+
559
+ ### 完成
560
+ - [ ] 所有测试通过
561
+ - [ ] 代码审查
562
+ - [ ] 更新 `LOOM_2.0_DEVELOPMENT_PLAN.md`
563
+ - [ ] 更新 `loom/tasks/README.md`
564
+
565
+ ---
566
+
567
+ ## 🔗 参考资源
568
+
569
+ - [Task 1.1: AgentEvent 模型](task_1.1_agent_events.md)
570
+ - [Task 1.2: 流式 API](task_1.2_streaming_api.md)
571
+ - [原始 Bug 报告](../../../LOOM_2.0_DEVELOPMENT_PLAN.md#原始问题)
572
+
573
+ ---
574
+
575
+ ## 📝 注意事项
576
+
577
+ ### 关键决策
578
+
579
+ 1. **优先级系统**: 使用 0-100 整数,而非枚举
580
+ - 灵活性更高
581
+ - 允许精细调整
582
+
583
+ 2. **Token 计数**: 使用简单估算
584
+ - 精确计数需要 tokenizer(依赖特定模型)
585
+ - 简单估算足够满足需求
586
+
587
+ 3. **截断策略**: 保守截断低优先级组件
588
+ - 保证关键信息完整
589
+ - 避免语义破坏
590
+
591
+ ### 潜在问题
592
+
593
+ 1. **Token 估算不准确**: 可能导致超出实际限制
594
+ - 解决:添加 10% buffer
595
+
596
+ 2. **组件顺序**: 某些 LLM 对 prompt 顺序敏感
597
+ - 解决:提供自定义排序选项(后续优化)
598
+
599
+ 3. **性能**: 大量组件时性能可能下降
600
+ - 解决:当前规模足够,后续可优化
601
+
602
+ ---
603
+
604
+ **创建日期**: 2025-10-25
605
+ **预计开始**: 2025-10-25
606
+ **预计完成**: 2025-10-26