Charles Grandjean commited on
Commit
acc767b
Β·
1 Parent(s): 984df29
subagents/doc_editor.py CHANGED
@@ -13,7 +13,7 @@ from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, Tool
13
  from agent_states.doc_editor_state import DocEditorState
14
  from utils.doc_editor_tools import (
15
  replace_html, add_html, delete_html, inspect_document, attempt_completion,
16
- _replace_html, _add_html, _delete_html
17
  )
18
  from prompts.doc_editor import get_doc_editor_system_prompt, get_summary_system_prompt
19
 
@@ -40,7 +40,7 @@ class DocumentEditorAgent:
40
  """
41
  self.llm = llm
42
  # Real tools (with _ prefix for internal functions) - these are actually called
43
- self.tools_real = [_replace_html, _add_html, _delete_html, inspect_document, attempt_completion]
44
  # Facade tools (exposed to LLM without doc_text parameter)
45
  self.tools_facade = [replace_html, add_html, delete_html, inspect_document, attempt_completion]
46
  self.llm_with_tools = self.llm.bind_tools(self.tools_facade)
@@ -181,25 +181,14 @@ class DocumentEditorAgent:
181
  for tool_call in last_message.tool_calls:
182
  tool_name = tool_call['name']
183
 
184
- # Special case: inspect_document - add doc_text to message stack
185
- if tool_name == "inspect_document":
186
- intermediate_steps.append(
187
- ToolMessage(
188
- content=f"Current document:\n{state['doc_text']}",
189
- tool_call_id=tool_call['id'],
190
- name=tool_name
191
- )
192
- )
193
- continue
194
-
195
  # Get the real tool function (with _ prefix)
196
  real_tool_func = next((t for t in self.tools_real if t.__name__ == "_" + tool_name), None)
197
 
198
  if real_tool_func:
199
  args = tool_call['args'].copy()
200
 
201
- # Inject doc_text for editing tools
202
- if tool_name in ["replace_html", "add_html", "delete_html"]:
203
  args["doc_text"] = state["doc_text"]
204
  logger.info(f"πŸ“ Injecting doc_text ({len(state['doc_text'])}b) into {tool_name}")
205
 
 
13
  from agent_states.doc_editor_state import DocEditorState
14
  from utils.doc_editor_tools import (
15
  replace_html, add_html, delete_html, inspect_document, attempt_completion,
16
+ _replace_html, _add_html, _delete_html, _inspect_document, _attempt_completion
17
  )
18
  from prompts.doc_editor import get_doc_editor_system_prompt, get_summary_system_prompt
19
 
 
40
  """
41
  self.llm = llm
42
  # Real tools (with _ prefix for internal functions) - these are actually called
43
+ self.tools_real = [_replace_html, _add_html, _delete_html, _inspect_document, _attempt_completion]
44
  # Facade tools (exposed to LLM without doc_text parameter)
45
  self.tools_facade = [replace_html, add_html, delete_html, inspect_document, attempt_completion]
46
  self.llm_with_tools = self.llm.bind_tools(self.tools_facade)
 
181
  for tool_call in last_message.tool_calls:
182
  tool_name = tool_call['name']
183
 
 
 
 
 
 
 
 
 
 
 
 
184
  # Get the real tool function (with _ prefix)
185
  real_tool_func = next((t for t in self.tools_real if t.__name__ == "_" + tool_name), None)
186
 
187
  if real_tool_func:
188
  args = tool_call['args'].copy()
189
 
190
+ # Inject doc_text for editing tools and inspect_document
191
+ if tool_name in ["replace_html", "add_html", "delete_html", "inspect_document"]:
192
  args["doc_text"] = state["doc_text"]
193
  logger.info(f"πŸ“ Injecting doc_text ({len(state['doc_text'])}b) into {tool_name}")
194
 
tests/test_bug_fixes.py ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script to verify bug fixes for document editor tools
4
+ Tests that:
5
+ 1. All internal functions exist and have __name__ attribute
6
+ 2. tools_real contains only functions (no StructuredTools)
7
+ 3. Workflow builds successfully
8
+ """
9
+
10
+ import sys
11
+ import os
12
+ from pathlib import Path
13
+
14
+ # Add parent directory to path
15
+ sys.path.insert(0, str(Path(__file__).parent.parent))
16
+
17
+ def test_internal_functions_exist():
18
+ """Test that all internal functions exist and have __name__"""
19
+ print("\n" + "=" * 80)
20
+ print("TEST 1: Internal Functions Exist and Have __name__")
21
+ print("=" * 80)
22
+
23
+ from utils.doc_editor_tools import (
24
+ _replace_html, _add_html, _delete_html, _inspect_document, _attempt_completion
25
+ )
26
+
27
+ functions = [
28
+ ("_replace_html", _replace_html),
29
+ ("_add_html", _add_html),
30
+ ("_delete_html", _delete_html),
31
+ ("_inspect_document", _inspect_document),
32
+ ("_attempt_completion", _attempt_completion)
33
+ ]
34
+
35
+ all_passed = True
36
+ for name, func in functions:
37
+ try:
38
+ func_name = func.__name__
39
+ print(f"βœ… {name}: __name__ = '{func_name}'")
40
+ except AttributeError as e:
41
+ print(f"❌ {name}: {e}")
42
+ all_passed = False
43
+
44
+ return all_passed
45
+
46
+
47
+ def test_tools_real_are_functions():
48
+ """Test that tools_real contains only functions"""
49
+ print("\n" + "=" * 80)
50
+ print("TEST 2: tools_real Contains Only Functions")
51
+ print("=" * 80)
52
+
53
+ # Import agent - this will fail if tools_real has StructuredTools
54
+ try:
55
+ from subagents.doc_editor import DocumentEditorAgent
56
+
57
+ # We need a mock LLM to initialize
58
+ class MockLLM:
59
+ def bind_tools(self, tools):
60
+ return self
61
+
62
+ async def ainvoke(self, messages):
63
+ from langchain_core.messages import AIMessage
64
+ return AIMessage(content="Test response")
65
+
66
+ llm = MockLLM()
67
+ agent = DocumentEditorAgent(llm=llm)
68
+
69
+ print(f"βœ… Agent initialized successfully")
70
+ print(f"πŸ“¦ tools_real has {len(agent.tools_real)} items")
71
+
72
+ # Check that all are functions
73
+ all_passed = True
74
+ for i, tool in enumerate(agent.tools_real):
75
+ try:
76
+ name = tool.__name__
77
+ print(f"βœ… Tool {i}: {name} (has __name__)")
78
+ except AttributeError as e:
79
+ print(f"❌ Tool {i}: {e}")
80
+ all_passed = False
81
+
82
+ return all_passed
83
+
84
+ except Exception as e:
85
+ print(f"❌ Failed to initialize agent: {e}")
86
+ import traceback
87
+ traceback.print_exc()
88
+ return False
89
+
90
+
91
+ def test_workflow_builds():
92
+ """Test that workflow builds successfully"""
93
+ print("\n" + "=" * 80)
94
+ print("TEST 3: Workflow Builds Successfully")
95
+ print("=" * 80)
96
+
97
+ try:
98
+ from subagents.doc_editor import DocumentEditorAgent
99
+
100
+ class MockLLM:
101
+ def bind_tools(self, tools):
102
+ return self
103
+
104
+ async def ainvoke(self, messages):
105
+ from langchain_core.messages import AIMessage
106
+ return AIMessage(content="Test response")
107
+
108
+ llm = MockLLM()
109
+ agent = DocumentEditorAgent(llm=llm)
110
+
111
+ print(f"βœ… Workflow built successfully")
112
+ # Note: CompiledStateGraph doesn't expose nodes() and edges() methods directly
113
+ # The important thing is that it built without errors
114
+
115
+ return True
116
+
117
+ except Exception as e:
118
+ print(f"❌ Failed to build workflow: {e}")
119
+ import traceback
120
+ traceback.print_exc()
121
+ return False
122
+
123
+
124
+ def test_tools_callable():
125
+ """Test that internal tools are callable"""
126
+ print("\n" + "=" * 80)
127
+ print("TEST 4: Internal Tools Are Callable")
128
+ print("=" * 80)
129
+
130
+ from utils.doc_editor_tools import (
131
+ _replace_html, _add_html, _delete_html, _inspect_document, _attempt_completion
132
+ )
133
+
134
+ import asyncio
135
+
136
+ # Test _replace_html
137
+ async def test_replace():
138
+ result = await _replace_html(
139
+ doc_text="Hello World",
140
+ search="World",
141
+ replace="Universe",
142
+ expected_matches=1
143
+ )
144
+ return result
145
+
146
+ # Test _inspect_document
147
+ async def test_inspect():
148
+ result = await _inspect_document(
149
+ doc_text="Test document"
150
+ )
151
+ return result
152
+
153
+ # Test _attempt_completion
154
+ async def test_attempt():
155
+ result = await _attempt_completion(
156
+ message="Test complete"
157
+ )
158
+ return result
159
+
160
+ all_passed = True
161
+
162
+ # Run tests
163
+ print("\nTesting _replace_html...")
164
+ try:
165
+ result = asyncio.run(test_replace())
166
+ if result.get("ok"):
167
+ print(f"βœ… _replace_html returned: {result}")
168
+ else:
169
+ print(f"⚠️ _replace_html failed (expected for invalid HTML): {result}")
170
+ except Exception as e:
171
+ print(f"❌ _replace_html error: {e}")
172
+ all_passed = False
173
+
174
+ print("\nTesting _inspect_document...")
175
+ try:
176
+ result = asyncio.run(test_inspect())
177
+ if result.get("ok"):
178
+ print(f"βœ… _inspect_document returned: {result}")
179
+ else:
180
+ print(f"❌ _inspect_document failed: {result}")
181
+ all_passed = False
182
+ except Exception as e:
183
+ print(f"❌ _inspect_document error: {e}")
184
+ all_passed = False
185
+
186
+ print("\nTesting _attempt_completion...")
187
+ try:
188
+ result = asyncio.run(test_attempt())
189
+ if result.get("ok"):
190
+ print(f"βœ… _attempt_completion returned: {result}")
191
+ else:
192
+ print(f"❌ _attempt_completion failed: {result}")
193
+ all_passed = False
194
+ except Exception as e:
195
+ print(f"❌ _attempt_completion error: {e}")
196
+ all_passed = False
197
+
198
+ return all_passed
199
+
200
+
201
+ if __name__ == "__main__":
202
+ print("\n" + "=" * 80)
203
+ print("BUG FIX VERIFICATION TESTS")
204
+ print("=" * 80)
205
+
206
+ results = {}
207
+
208
+ # Test 1: Internal functions exist
209
+ results["test_internal_functions_exist"] = test_internal_functions_exist()
210
+
211
+ # Test 2: tools_real contains only functions
212
+ results["test_tools_real_are_functions"] = test_tools_real_are_functions()
213
+
214
+ # Test 3: Workflow builds
215
+ results["test_workflow_builds"] = test_workflow_builds()
216
+
217
+ # Test 4: Tools are callable
218
+ results["test_tools_callable"] = test_tools_callable()
219
+
220
+ # Summary
221
+ print("\n" + "=" * 80)
222
+ print("TEST SUMMARY")
223
+ print("=" * 80)
224
+
225
+ passed = sum(1 for v in results.values() if v)
226
+ total = len(results)
227
+
228
+ for test_name, result in results.items():
229
+ status = "βœ… PASSED" if result else "❌ FAILED"
230
+ print(f"{status}: {test_name}")
231
+
232
+ print("\n" + "=" * 80)
233
+ if passed == total:
234
+ print(f"βœ… ALL TESTS PASSED ({passed}/{total})")
235
+ else:
236
+ print(f"❌ SOME TESTS FAILED ({passed}/{total} passed)")
237
+ print("=" * 80 + "\n")
238
+
239
+ sys.exit(0 if passed == total else 1)
utils/doc_editor_tools.py CHANGED
@@ -187,6 +187,44 @@ async def _delete_html(doc_text: str, search: str, expected_matches: int = 1) ->
187
  return await _replace_html(doc_text, search, "", expected_matches)
188
 
189
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  @tool
191
  async def replace_html(search: str, replace: str, expected_matches: int = 1) -> Dict[str, Any]:
192
  """
 
187
  return await _replace_html(doc_text, search, "", expected_matches)
188
 
189
 
190
+ async def _inspect_document(doc_text: str) -> Dict[str, Any]:
191
+ """
192
+ Internal implementation of inspect_document (with doc_text parameter).
193
+
194
+ Returns the current document state for inspection.
195
+
196
+ Args:
197
+ doc_text: The HTML document content
198
+
199
+ Returns:
200
+ Dict with 'ok' (bool) and 'content' (str)
201
+ """
202
+ logger.info(f" πŸ” inspect_document | size:{len(doc_text)}b")
203
+ return {
204
+ "ok": True,
205
+ "content": doc_text
206
+ }
207
+
208
+
209
+ async def _attempt_completion(message: str) -> Dict[str, Any]:
210
+ """
211
+ Internal implementation of attempt_completion.
212
+
213
+ Signals that document editing is complete.
214
+
215
+ Args:
216
+ message: Summary message describing what was changed
217
+
218
+ Returns:
219
+ Dict with 'ok' (bool) and 'message' (str)
220
+ """
221
+ logger.info(f" βœ… attempt_completion | {message}")
222
+ return {
223
+ "ok": True,
224
+ "message": message
225
+ }
226
+
227
+
228
  @tool
229
  async def replace_html(search: str, replace: str, expected_matches: int = 1) -> Dict[str, Any]:
230
  """