Charles Grandjean commited on
Commit
e13ea3d
·
1 Parent(s): 96ab95b

doc editor revamp

Browse files
prompts/doc_editor.py CHANGED
@@ -88,26 +88,24 @@ Example:
88
  }
89
  ```
90
 
91
- ### inspect_document
92
- View the current document state in the conversation history.
93
-
94
- Use this tool to see the current document content after previous edits. This adds the document to the message history so you can verify modifications and understand the current structure before making further changes.
95
 
96
  Parameters:
97
- - (no parameters - document is provided automatically)
98
 
99
  Example:
100
  ```json
101
  {
102
  "type": "tool_call",
103
- "name": "inspect_document",
104
  "arguments": {}
105
  }
106
  ```
107
 
108
  ### attempt_completion
109
  Signal that all modifications are complete.
110
-
111
  Parameters:
112
  - message: A summary message describing what was changed
113
 
 
88
  }
89
  ```
90
 
91
+ ### view_current_document
92
+ View the current state of the document being edited.
93
+ Use this tool to see the current document content after modifications. This helps you verify previous edits and understand the current structure.
 
94
 
95
  Parameters:
96
+ - (no parameters - document content is provided automatically)
97
 
98
  Example:
99
  ```json
100
  {
101
  "type": "tool_call",
102
+ "name": "view_current_document",
103
  "arguments": {}
104
  }
105
  ```
106
 
107
  ### attempt_completion
108
  Signal that all modifications are complete.
 
109
  Parameters:
110
  - message: A summary message describing what was changed
111
 
subagents/doc_editor.py CHANGED
@@ -3,7 +3,7 @@
3
  Document Editor Agent - LangGraph agent for modifying HTML documents
4
  Implements Cline-like iterative editing with validation
5
  """
6
-
7
  import logging
8
  import traceback
9
  from typing import Dict, Any, List, Optional
@@ -12,7 +12,7 @@ from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, Tool
12
 
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
  )
17
  from prompts.doc_editor import get_doc_editor_system_prompt, get_summary_system_prompt
18
  from utils.update_notifier import push_document_update
@@ -21,30 +21,19 @@ logger = logging.getLogger(__name__)
21
 
22
 
23
  class DocumentEditorAgent:
24
- """
25
- Agent for editing HTML documents using Cline-like iterative approach.
26
-
27
- Workflow:
28
- 1. Agent generates a tool call (replace_html/add_html/delete_html/attempt_completion)
29
- 2. Tools execute with BeautifulSoup validation
30
- 3. Check if complete or max iterations reached
31
- 4. Repeat until completion or error
32
- """
33
 
34
  def __init__(self, llm, llm_tool_calling):
35
  """
36
  Initialize the document editor agent.
37
 
38
  Args:
39
- llm: LLM principal (OpenAI) pour la génération du résumé final
40
- llm_tool_calling: LLM (Gemini) pour les tool calls
41
  """
42
- self.llm = llm # OpenAI pour le résumé
43
- self.llm_tool_calling = llm_tool_calling # Gemini pour les tool calls
44
- # All tools are LangChain tools with @tool decorator
45
- self.tools = [replace_html, add_html, delete_html, inspect_document, attempt_completion]
46
- # Force at least one tool call at each iteration using tool_choice="any"
47
- # Use Gemini for tool calling (better quality output)
48
  self.llm_with_tools = self.llm_tool_calling.bind_tools(self.tools, tool_choice="any")
49
  logger.info("🔧 Tool binding configured with tool_choice='any' to force tool calls")
50
  logger.info(f"🤖 Using {type(llm_tool_calling).__name__} for tool calling")
@@ -58,32 +47,18 @@ class DocumentEditorAgent:
58
  workflow.add_node("tools", self._tools_node)
59
  workflow.add_node("summary", self._summary_node)
60
  workflow.set_entry_point("agent")
61
- workflow.add_conditional_edges(
62
- "agent",
63
- self._should_continue,
64
- {"continue": "tools", "end": END}
65
- )
66
- workflow.add_conditional_edges(
67
- "tools",
68
- self._after_tools,
69
- {"continue": "agent", "summary": "summary", "end": END}
70
- )
71
  workflow.add_edge("summary", END)
72
  return workflow.compile()
73
 
74
  def _should_continue(self, state: DocEditorState) -> str:
75
- """
76
- Decide whether to continue after agent node.
77
-
78
- Returns:
79
- "continue" if agent made tool calls or if attempt_completion not yet called, "end" if complete
80
- """
81
- # Check max iterations first - stop regardless of tool calls
82
  iteration_count = state.get("iteration_count", 0)
83
  max_iterations = state.get("max_iterations", 10)
84
 
85
  if iteration_count >= max_iterations:
86
- logger.warning(f"⚠️ Max iterations ({max_iterations}) reached - ending workflow")
87
  return "end"
88
 
89
  intermediate_steps = state.get("intermediate_steps", [])
@@ -92,53 +67,40 @@ class DocumentEditorAgent:
92
 
93
  last_message = intermediate_steps[-1]
94
 
95
- # Check if last message has tool calls
96
  if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
97
  tool_names = [tc['name'] for tc in last_message.tool_calls]
98
- logger.info(f"🔧 Agent calling tools: {tool_names}")
99
  return "continue"
100
 
101
- # Check if attempt_completion was ever called successfully
102
- attempt_completion_called = False
103
- for msg in intermediate_steps:
104
- if isinstance(msg, ToolMessage) and msg.name == "attempt_completion":
105
- attempt_completion_called = True
106
- break
107
 
108
  if attempt_completion_called:
109
- logger.info("attempt_completion already called - ending workflow")
110
  return "end"
111
 
112
- # Agent didn't make tool calls but also didn't complete
113
- # Increment iteration count was already done in _agent_node, so just continue
114
- logger.warning("⚠️ Agent response without tool calls - forcing continue (should call attempt_completion)")
115
  return "continue"
116
 
117
  def _after_tools(self, state: DocEditorState) -> str:
118
- """
119
- Decide whether to continue after tools node.
120
-
121
- Returns:
122
- "continue" if more iterations needed, "summary" if complete, "end" if max iterations reached
123
- """
124
  intermediate_steps = state.get("intermediate_steps", [])
125
-
126
  last_msg = intermediate_steps[-1]
127
- if isinstance(last_msg, ToolMessage):
128
- # Rule 1: If last tool was attempt_completion → go to summary
129
- if last_msg.name == "attempt_completion":
130
- logger.info("✅ attempt_completion called - generating summary")
131
- return "summary"
132
 
133
- # Rule 2: If beyond max_iterations → stop
 
 
 
134
  iteration_count = state.get("iteration_count", 0)
135
  max_iterations = state.get("max_iterations", 10)
136
 
137
  if iteration_count >= max_iterations:
138
- logger.warning(f"⚠️ Max iterations ({max_iterations}) reached - ending workflow")
139
  return "end"
140
 
141
- # Rule 3: Everything else → continue
142
  return "continue"
143
 
144
  async def _agent_node(self, state: DocEditorState) -> DocEditorState:
@@ -147,13 +109,23 @@ class DocumentEditorAgent:
147
  iteration_count = state.get("iteration_count", 0)
148
  max_iterations = state.get("max_iterations", 10)
149
 
150
- logger.info(f"🔄 Agent iteration {iteration_count + 1}/{max_iterations}")
151
 
152
- # First iteration: add system prompt and user instruction
153
  if iteration_count == 0:
154
  intermediate_steps.append(SystemMessage(content=get_doc_editor_system_prompt()))
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
- # Build context
157
  context_msg = ""
158
  conversation_history = state.get("conversation_history", [])
159
  doc_summaries = state.get("doc_summaries", [])
@@ -165,21 +137,16 @@ class DocumentEditorAgent:
165
 
166
  if doc_summaries:
167
  context_msg += "\n\nDocument summaries:\n" + "\n".join(f"- {s}" for s in doc_summaries)
168
- plan_formatted=f"""## EXECUTION PLAN
 
169
  You have been provided with an execution plan. Follow this plan carefully to complete the task:
170
  {state['plan']}
171
  Use this plan as your guide for the editing steps to perform."""
172
- # Add document and instruction
173
- full_message = (
174
- f"Current document (HTML):\n{state['doc_text']}\n\n"
175
- f"{context_msg}\n\n"
176
- f"Instruction: {state['user_instruction']}"
177
- f"{plan_formatted}"
178
- )
179
  intermediate_steps.append(HumanMessage(content=full_message))
180
- logger.info(f"📚 Context: {len(conversation_history)} hist + {len(doc_summaries)} summaries")
181
 
182
- # Call LLM
183
  response = await self.llm_with_tools.ainvoke(intermediate_steps)
184
  intermediate_steps.append(response)
185
 
@@ -196,76 +163,58 @@ Use this plan as your guide for the editing steps to perform."""
196
 
197
  for tool_call in last_message.tool_calls:
198
  tool_name = tool_call['name']
199
-
200
- # Get the tool function directly from self.tools
201
  tool_func = next((t for t in self.tools if t.name == tool_name), None)
202
 
203
- if tool_func:
204
- args = tool_call['args'].copy()
205
-
206
- # Inject doc_text for editing tools that need it
207
- if tool_name in ["replace_html", "add_html", "delete_html"]:
208
- args["doc_text"] = state["doc_text"]
209
- logger.info(f"📝 Injecting doc_text ({len(state['doc_text'])}b) into {tool_name}")
210
-
211
- # Special handling for inspect_document - inject doc_text and return content
212
- if tool_name == "inspect_document":
213
- args["doc_text"] = state["doc_text"]
214
- logger.info(f"🔍 Inspecting document ({len(state['doc_text'])}b)")
 
 
 
 
215
 
216
- try:
217
- # Call the LangChain tool with .ainvoke()
218
- result = await tool_func.ainvoke(args)
219
 
220
- # Update doc_text if tool was successful
221
- if result.get("ok") and "doc_text" in result:
222
- state["doc_text"] = result["doc_text"]
223
 
224
- # Push update to external endpoint if tool modified document successfully
225
- # Only for editing tools (replace_html, add_html, delete_html)
226
- if tool_name in ["replace_html", "add_html", "delete_html"]:
227
- document_id = state.get("document_id")
228
- user_id = state.get("user_id")
229
-
230
- if document_id and user_id:
231
- logger.info(f"📤 Pushing document update after successful {tool_name}...")
232
- # Async but await - continue editing regardless of success/failure
233
- await push_document_update(
234
- document_id=document_id,
235
- content=state["doc_text"],
236
- user_id=user_id
237
- )
238
- else:
239
- logger.debug(f"ℹ️ No document_id/user_id, skipping update push")
240
-
241
- # Add result to conversation
242
- intermediate_steps.append(
243
- ToolMessage(
244
- content=str(result),
245
- tool_call_id=tool_call['id'],
246
- name=tool_name
247
- )
248
- )
249
- except Exception as e:
250
- intermediate_steps.append(
251
- ToolMessage(
252
- content=f"Error: {str(e)}",
253
- tool_call_id=tool_call['id'],
254
- name=tool_name
255
- )
256
- )
257
- logger.error(f"❌ {tool_name} error: {str(e)}")
258
- else:
259
- logger.warning(f"⚠️ Tool function not found for {tool_name}")
260
 
261
  state["intermediate_steps"] = intermediate_steps
262
  return state
263
 
264
  async def _summary_node(self, state: DocEditorState) -> DocEditorState:
265
  """Summary node: Generate a clean summary of all modifications."""
266
- logger.info("📝 Generating modification summary...")
267
 
268
- # Build context for the summary - include the entire conversation
269
  summary_messages = [
270
  SystemMessage(content=get_summary_system_prompt()),
271
  HumanMessage(content=f"""
@@ -275,17 +224,15 @@ Full conversation history (including all tool calls and results):
275
  """, name="user")
276
  ]
277
 
278
- # Add ALL messages from intermediate_steps (complete conversation)
279
  intermediate_steps = state.get("intermediate_steps", [])
280
  for msg in intermediate_steps:
281
  summary_messages.append(msg)
282
 
283
- # Generate summary
284
  response = await self.llm.ainvoke(summary_messages)
285
  state["final_summary"] = response.content
286
 
287
- logger.info("Summary generated successfully")
288
- logger.info(f"📋 Summary preview: {response.content[:200]}...")
289
 
290
  return state
291
 
@@ -314,37 +261,31 @@ Full conversation history (including all tool calls and results):
314
  user_id: Optional user ID for authentication
315
 
316
  Returns:
317
- Dict with:
318
- - doc_text: Modified document (HTML)
319
- - message: Completion message or error description
320
- - success: Boolean indicating success
321
- - iteration_count: Number of iterations performed
322
  """
323
- # Log initial state
324
  logger.info("=" * 80)
325
- logger.info("🎯 DOCUMENT EDITOR AGENT STARTING")
326
  logger.info("=" * 80)
327
- logger.info(f"📏 Initial document size: {len(doc_text)} bytes")
328
- logger.info(f"📋 Instruction: {user_instruction[:100]}{'...' if len(user_instruction) > 100 else ''}")
329
- logger.info(f"📚 Document summaries: {len(doc_summaries)}")
330
- logger.info(f"💬 Conversation history: {len(conversation_history)} messages")
331
- logger.info(f"🔄 Max iterations: {max_iterations}")
332
  if document_id:
333
- logger.info(f"🆔 Document ID: {document_id} (live updates enabled)")
334
  if user_id:
335
- logger.info(f"👤 User ID: {user_id}")
336
 
337
  if doc_summaries:
338
- logger.info("📚 Document summaries loaded:")
339
- for i, summary in enumerate(doc_summaries[:3], 1): # Show first 3
340
- logger.info(f" [{i}] {str(summary)[:100]}...")
341
  if len(doc_summaries) > 3:
342
  logger.info(f" ... and {len(doc_summaries) - 3} more")
343
 
344
  if conversation_history:
345
- logger.info(f"Conversation history loaded ({len(conversation_history)} messages)")
346
 
347
- # Initialize state
348
  initial_state = {
349
  "doc_text": doc_text,
350
  "doc_summaries": doc_summaries,
@@ -358,49 +299,42 @@ Full conversation history (including all tool calls and results):
358
  "user_id": user_id
359
  }
360
 
361
- logger.info("🎯 Initial state prepared, starting workflow...")
362
 
363
- # Run workflow
364
  try:
365
- logger.info("🔄 Invoking LangGraph workflow...")
366
  final_state = await self.workflow.ainvoke(initial_state)
367
 
368
- # Prepare result
369
  final_summary = final_state.get("final_summary", "")
370
- # Success if attempt_completion was called (which triggers summary)
371
- attempt_completion_called = False
372
- for msg in final_state.get("intermediate_steps", []):
373
- if isinstance(msg, ToolMessage) and msg.name == "attempt_completion":
374
- attempt_completion_called = True
375
- break
376
  success = attempt_completion_called
377
- # Use final_summary as the main message for the frontend
378
- message = final_summary
379
 
380
  iteration_count = final_state.get("iteration_count", 0)
381
  final_doc_size = len(final_state["doc_text"])
382
  size_change = final_doc_size - len(doc_text)
383
 
384
  logger.info("=" * 80)
385
- logger.info("📊 DOCUMENT EDITING COMPLETED")
386
  logger.info("=" * 80)
387
- logger.info(f"Success: {success}")
388
- logger.info(f"🔄 Iterations: {iteration_count}")
389
- logger.info(f"📏 Final document size: {final_doc_size} bytes")
390
- logger.info(f"📈 Size change: {size_change:+d} bytes ({size_change/len(doc_text)*100:+.1f}%)")
391
- logger.info(f"💬 Message: {message[:100]}{'...' if len(message) > 100 else ''}")
392
 
393
- # Log final document content
394
  logger.info("=" * 80)
395
- logger.info("📄 FINAL DOCUMENT CONTENT")
396
  logger.info("=" * 80)
397
  logger.info(final_state["doc_text"])
398
  logger.info("=" * 80)
399
 
400
- # Log summary
401
  if final_summary:
402
  logger.info("=" * 80)
403
- logger.info("📋 MODIFICATION SUMMARY")
404
  logger.info("=" * 80)
405
  logger.info(final_summary)
406
  logger.info("=" * 80)
@@ -408,7 +342,7 @@ Full conversation history (including all tool calls and results):
408
  if not success:
409
  max_iters = final_state.get("max_iterations", 10)
410
  if iteration_count >= max_iters:
411
- logger.warning(f"⚠️ Failed to complete editing within {max_iters} iterations")
412
  message = f"Failed to complete editing within {max_iters} iterations"
413
 
414
  return {
@@ -421,42 +355,33 @@ Full conversation history (including all tool calls and results):
421
 
422
  except Exception as e:
423
  logger.error("=" * 80)
424
- logger.error("DOCUMENT EDITING FAILED")
425
  logger.error("=" * 80)
426
- logger.error(f"📍 Location: subagents/doc_editor.py:{traceback.extract_tb(e.__traceback__)[-1].lineno} (edit_document)")
427
- logger.error("")
428
- logger.error("📋 Input Parameters:")
429
- logger.error(f" - User Instruction: {user_instruction[:100] if len(user_instruction) > 100 else user_instruction}")
430
- logger.error(f" - Document Size: {len(doc_text):,} bytes")
431
  if document_id:
432
- logger.error(f" - Document ID: {document_id}")
433
  if user_id:
434
- logger.error(f" - User ID: {user_id}")
435
- logger.error(f" - Max Iterations: {max_iterations}")
436
- logger.error(f" - Document Summaries: {len(doc_summaries)}")
437
- logger.error(f" - Conversation History: {len(conversation_history)} messages")
438
  if plan:
439
- logger.error(f" - Plan: {plan[:200] if len(plan) > 200 else plan}")
440
- logger.error("")
441
- logger.error("🤖 LLM Configuration:")
442
- logger.error(f" - Main LLM: {type(self.llm).__name__}")
443
- logger.error(f" - Tool Calling LLM: {type(self.llm_tool_calling).__name__}")
444
- logger.error(f" - Tools Available: {len(self.tools)}")
445
- logger.error(f" - Tool Names: {', '.join([t.name for t in self.tools])}")
446
- logger.error("")
447
- logger.error("🔍 Exception Details:")
448
- logger.error(f" Type: {type(e).__name__}")
449
- logger.error(f" Message: {str(e)}")
450
- logger.error("")
451
- logger.error("📝 Full Traceback:")
452
- logger.error(traceback.format_exc())
453
- logger.error("")
454
- logger.error("💾 Document Preview (first 200 chars):")
455
- logger.error(f" {doc_text[:200]}")
456
  logger.error("=" * 80)
 
457
  return {
458
- "doc_text": doc_text, # Return original on error
459
  "message": f"Error during editing: {str(e)}",
460
  "success": False,
461
  "iteration_count": 0
462
- }
 
3
  Document Editor Agent - LangGraph agent for modifying HTML documents
4
  Implements Cline-like iterative editing with validation
5
  """
6
+ import uuid
7
  import logging
8
  import traceback
9
  from typing import Dict, Any, List, Optional
 
12
 
13
  from agent_states.doc_editor_state import DocEditorState
14
  from utils.doc_editor_tools import (
15
+ replace_html, add_html, delete_html, view_current_document, attempt_completion
16
  )
17
  from prompts.doc_editor import get_doc_editor_system_prompt, get_summary_system_prompt
18
  from utils.update_notifier import push_document_update
 
21
 
22
 
23
  class DocumentEditorAgent:
24
+ """Agent for editing HTML documents using Cline-like iterative approach."""
 
 
 
 
 
 
 
 
25
 
26
  def __init__(self, llm, llm_tool_calling):
27
  """
28
  Initialize the document editor agent.
29
 
30
  Args:
31
+ llm: LLM principal pour la génération du résumé final
32
+ llm_tool_calling: LLM pour les tool calls
33
  """
34
+ self.llm = llm
35
+ self.llm_tool_calling = llm_tool_calling
36
+ self.tools = [replace_html, add_html, delete_html, view_current_document, attempt_completion]
 
 
 
37
  self.llm_with_tools = self.llm_tool_calling.bind_tools(self.tools, tool_choice="any")
38
  logger.info("🔧 Tool binding configured with tool_choice='any' to force tool calls")
39
  logger.info(f"🤖 Using {type(llm_tool_calling).__name__} for tool calling")
 
47
  workflow.add_node("tools", self._tools_node)
48
  workflow.add_node("summary", self._summary_node)
49
  workflow.set_entry_point("agent")
50
+ workflow.add_conditional_edges("agent", self._should_continue, {"continue": "tools", "end": END})
51
+ workflow.add_conditional_edges("tools", self._after_tools, {"continue": "agent", "summary": "summary", "end": END})
 
 
 
 
 
 
 
 
52
  workflow.add_edge("summary", END)
53
  return workflow.compile()
54
 
55
  def _should_continue(self, state: DocEditorState) -> str:
56
+ """Decide whether to continue after agent node."""
 
 
 
 
 
 
57
  iteration_count = state.get("iteration_count", 0)
58
  max_iterations = state.get("max_iterations", 10)
59
 
60
  if iteration_count >= max_iterations:
61
+ logger.warning(f"Max iterations ({max_iterations}) reached - ending workflow")
62
  return "end"
63
 
64
  intermediate_steps = state.get("intermediate_steps", [])
 
67
 
68
  last_message = intermediate_steps[-1]
69
 
 
70
  if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
71
  tool_names = [tc['name'] for tc in last_message.tool_calls]
72
+ logger.info(f"Agent calling tools: {tool_names}")
73
  return "continue"
74
 
75
+ # Check if attempt_completion was ever called
76
+ attempt_completion_called = any(
77
+ isinstance(msg, ToolMessage) and msg.name == "attempt_completion"
78
+ for msg in intermediate_steps
79
+ )
 
80
 
81
  if attempt_completion_called:
82
+ logger.info("attempt_completion already called - ending workflow")
83
  return "end"
84
 
85
+ logger.warning("Agent response without tool calls - forcing continue")
 
 
86
  return "continue"
87
 
88
  def _after_tools(self, state: DocEditorState) -> str:
89
+ """Decide whether to continue after tools node."""
 
 
 
 
 
90
  intermediate_steps = state.get("intermediate_steps", [])
 
91
  last_msg = intermediate_steps[-1]
 
 
 
 
 
92
 
93
+ if isinstance(last_msg, ToolMessage) and last_msg.name == "attempt_completion":
94
+ logger.info("attempt_completion called - generating summary")
95
+ return "summary"
96
+
97
  iteration_count = state.get("iteration_count", 0)
98
  max_iterations = state.get("max_iterations", 10)
99
 
100
  if iteration_count >= max_iterations:
101
+ logger.warning(f"Max iterations ({max_iterations}) reached - ending workflow")
102
  return "end"
103
 
 
104
  return "continue"
105
 
106
  async def _agent_node(self, state: DocEditorState) -> DocEditorState:
 
109
  iteration_count = state.get("iteration_count", 0)
110
  max_iterations = state.get("max_iterations", 10)
111
 
112
+ logger.info(f"Agent iteration {iteration_count + 1}/{max_iterations}")
113
 
 
114
  if iteration_count == 0:
115
  intermediate_steps.append(SystemMessage(content=get_doc_editor_system_prompt()))
116
+ fake_tool_call_id = str(uuid.uuid4())
117
+ intermediate_steps.append(
118
+ AIMessage(content="", tool_calls=[{
119
+ "id": fake_tool_call_id,
120
+ "name": "view_current_document",
121
+ "args": {}
122
+ }])
123
+ )
124
+ intermediate_steps.append(
125
+ ToolMessage(content=state['doc_text'], tool_call_id=fake_tool_call_id, name="view_current_document")
126
+ )
127
+ logger.info(f"🔍 Initial document provided via fake view_current_document ({len(state['doc_text'])}b)")
128
 
 
129
  context_msg = ""
130
  conversation_history = state.get("conversation_history", [])
131
  doc_summaries = state.get("doc_summaries", [])
 
137
 
138
  if doc_summaries:
139
  context_msg += "\n\nDocument summaries:\n" + "\n".join(f"- {s}" for s in doc_summaries)
140
+
141
+ plan_formatted = f"""## EXECUTION PLAN
142
  You have been provided with an execution plan. Follow this plan carefully to complete the task:
143
  {state['plan']}
144
  Use this plan as your guide for the editing steps to perform."""
145
+
146
+ full_message = f"{context_msg}\n\nInstruction: {state['user_instruction']}\n\n{plan_formatted}"
 
 
 
 
 
147
  intermediate_steps.append(HumanMessage(content=full_message))
148
+ logger.info(f"Context: {len(conversation_history)} hist + {len(doc_summaries)} summaries")
149
 
 
150
  response = await self.llm_with_tools.ainvoke(intermediate_steps)
151
  intermediate_steps.append(response)
152
 
 
163
 
164
  for tool_call in last_message.tool_calls:
165
  tool_name = tool_call['name']
 
 
166
  tool_func = next((t for t in self.tools if t.name == tool_name), None)
167
 
168
+ if not tool_func:
169
+ logger.warning(f"Tool function not found for {tool_name}")
170
+ continue
171
+
172
+ args = tool_call['args'].copy()
173
+
174
+ # Inject doc_text for editing tools
175
+ if tool_name in ["replace_html", "add_html", "delete_html"]:
176
+ args["doc_text"] = state["doc_text"]
177
+ logger.info(f"Injecting doc_text ({len(state['doc_text'])}b) into {tool_name}")
178
+
179
+ if tool_name == "view_current_document":
180
+ logger.info(f"Viewing current document ({len(state['doc_text'])}b)")
181
+
182
+ try:
183
+ result = await tool_func.ainvoke(args)
184
 
185
+ if result.get("ok") and "doc_text" in result:
186
+ state["doc_text"] = result["doc_text"]
 
187
 
188
+ if tool_name in ["replace_html", "add_html", "delete_html"]:
189
+ document_id = state.get("document_id")
190
+ user_id = state.get("user_id")
191
 
192
+ if document_id and user_id:
193
+ logger.info(f"Pushing document update after successful {tool_name}...")
194
+ await push_document_update(
195
+ document_id=document_id,
196
+ content=state["doc_text"],
197
+ user_id=user_id
198
+ )
199
+ else:
200
+ logger.debug("No document_id/user_id, skipping update push")
201
+
202
+ intermediate_steps.append(
203
+ ToolMessage(content=str(result), tool_call_id=tool_call['id'], name=tool_name)
204
+ )
205
+ except Exception as e:
206
+ intermediate_steps.append(
207
+ ToolMessage(content=f"Error: {str(e)}", tool_call_id=tool_call['id'], name=tool_name)
208
+ )
209
+ logger.error(f"{tool_name} error: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
 
211
  state["intermediate_steps"] = intermediate_steps
212
  return state
213
 
214
  async def _summary_node(self, state: DocEditorState) -> DocEditorState:
215
  """Summary node: Generate a clean summary of all modifications."""
216
+ logger.info("Generating modification summary...")
217
 
 
218
  summary_messages = [
219
  SystemMessage(content=get_summary_system_prompt()),
220
  HumanMessage(content=f"""
 
224
  """, name="user")
225
  ]
226
 
 
227
  intermediate_steps = state.get("intermediate_steps", [])
228
  for msg in intermediate_steps:
229
  summary_messages.append(msg)
230
 
 
231
  response = await self.llm.ainvoke(summary_messages)
232
  state["final_summary"] = response.content
233
 
234
+ logger.info("Summary generated successfully")
235
+ logger.info(f"Summary preview: {response.content}...")
236
 
237
  return state
238
 
 
261
  user_id: Optional user ID for authentication
262
 
263
  Returns:
264
+ Dict with doc_text, message, success, iteration_count, final_summary
 
 
 
 
265
  """
 
266
  logger.info("=" * 80)
267
+ logger.info("DOCUMENT EDITOR AGENT STARTING")
268
  logger.info("=" * 80)
269
+ logger.info(f"Initial document size: {len(doc_text)} bytes")
270
+ logger.info(f"Instruction: {user_instruction}{'...' if len(user_instruction) > 100 else ''}")
271
+ logger.info(f"Document summaries: {len(doc_summaries)}")
272
+ logger.info(f"Conversation history: {len(conversation_history)} messages")
273
+ logger.info(f"Max iterations: {max_iterations}")
274
  if document_id:
275
+ logger.info(f"Document ID: {document_id} (live updates enabled)")
276
  if user_id:
277
+ logger.info(f"User ID: {user_id}")
278
 
279
  if doc_summaries:
280
+ logger.info("Document summaries loaded:")
281
+ for i, summary in enumerate(doc_summaries, 1):
282
+ logger.info(f" [{i}] {str(summary)}...")
283
  if len(doc_summaries) > 3:
284
  logger.info(f" ... and {len(doc_summaries) - 3} more")
285
 
286
  if conversation_history:
287
+ logger.info(f"Conversation history loaded ({len(conversation_history)} messages)")
288
 
 
289
  initial_state = {
290
  "doc_text": doc_text,
291
  "doc_summaries": doc_summaries,
 
299
  "user_id": user_id
300
  }
301
 
302
+ logger.info("Initial state prepared, starting workflow...")
303
 
 
304
  try:
305
+ logger.info("Invoking LangGraph workflow...")
306
  final_state = await self.workflow.ainvoke(initial_state)
307
 
 
308
  final_summary = final_state.get("final_summary", "")
309
+ attempt_completion_called = any(
310
+ isinstance(msg, ToolMessage) and msg.name == "attempt_completion"
311
+ for msg in final_state.get("intermediate_steps", [])
312
+ )
 
 
313
  success = attempt_completion_called
314
+ message = final_summary
 
315
 
316
  iteration_count = final_state.get("iteration_count", 0)
317
  final_doc_size = len(final_state["doc_text"])
318
  size_change = final_doc_size - len(doc_text)
319
 
320
  logger.info("=" * 80)
321
+ logger.info("DOCUMENT EDITING COMPLETED")
322
  logger.info("=" * 80)
323
+ logger.info(f"Success: {success}")
324
+ logger.info(f"Iterations: {iteration_count}")
325
+ logger.info(f"Final document size: {final_doc_size} bytes")
326
+ logger.info(f"Size change: {size_change:+d} bytes ({size_change/len(doc_text)*100:+.1f}%)")
327
+ logger.info(f"Message: {message}{'...' if len(message) > 100 else ''}")
328
 
 
329
  logger.info("=" * 80)
330
+ logger.info("FINAL DOCUMENT CONTENT")
331
  logger.info("=" * 80)
332
  logger.info(final_state["doc_text"])
333
  logger.info("=" * 80)
334
 
 
335
  if final_summary:
336
  logger.info("=" * 80)
337
+ logger.info("MODIFICATION SUMMARY")
338
  logger.info("=" * 80)
339
  logger.info(final_summary)
340
  logger.info("=" * 80)
 
342
  if not success:
343
  max_iters = final_state.get("max_iterations", 10)
344
  if iteration_count >= max_iters:
345
+ logger.warning(f"Failed to complete editing within {max_iters} iterations")
346
  message = f"Failed to complete editing within {max_iters} iterations"
347
 
348
  return {
 
355
 
356
  except Exception as e:
357
  logger.error("=" * 80)
358
+ logger.error("DOCUMENT EDITING FAILED")
359
  logger.error("=" * 80)
360
+ logger.error(f"Location: subagents/doc_editor.py:{traceback.extract_tb(e.__traceback__)[-1].lineno}")
361
+ logger.error(f"Error type: {type(e).__name__}")
362
+ logger.error(f"Error message: {str(e)}")
363
+ logger.error(f"User Instruction: {user_instruction if len(user_instruction) > 100 else user_instruction}")
364
+ logger.error(f"Document Size: {len(doc_text):,} bytes")
365
  if document_id:
366
+ logger.error(f"Document ID: {document_id}")
367
  if user_id:
368
+ logger.error(f"User ID: {user_id}")
369
+ logger.error(f"Max Iterations: {max_iterations}")
370
+ logger.error(f"Document Summaries: {len(doc_summaries)}")
371
+ logger.error(f"Conversation History: {len(conversation_history)} messages")
372
  if plan:
373
+ logger.error(f"Plan: {plan if len(plan) > 200 else plan}")
374
+ logger.error(f"Main LLM: {type(self.llm).__name__}")
375
+ logger.error(f"Tool Calling LLM: {type(self.llm_tool_calling).__name__}")
376
+ logger.error(f"Tools Available: {len(self.tools)}")
377
+ logger.error(f"Tool Names: {', '.join([t.name for t in self.tools])}")
378
+ logger.error(f"Traceback:\n{traceback.format_exc()}")
379
+ logger.error(f"Document Preview: {doc_text[:200]}")
 
 
 
 
 
 
 
 
 
 
380
  logger.error("=" * 80)
381
+
382
  return {
383
+ "doc_text": doc_text,
384
  "message": f"Error during editing: {str(e)}",
385
  "success": False,
386
  "iteration_count": 0
387
+ }
utils/doc_editor_tools.py CHANGED
@@ -235,25 +235,23 @@ async def delete_html(doc_text: str, search: str, expected_matches: int = 1) ->
235
 
236
 
237
  @tool
238
- async def inspect_document(doc_text: str = None) -> Dict[str, Any]:
239
  """
240
- Return the current state of the document.
241
 
242
- Use this tool to see the current document content in the message history.
243
- This adds the document state to the conversation context so you can verify
244
- previous edits and understand the current structure.
245
 
246
- The document content will be automatically injected from the workflow state.
247
 
248
  Returns:
249
  Dict with 'ok' (bool) and 'content' (str) containing the HTML document
250
  """
251
- # Note: doc_text is injected by _tools_node in doc_editor.py
252
- # but we accept it as optional parameter for backward compatibility with tests
253
- logger.info(f" 🔍 inspect_document | size:{len(doc_text) if doc_text else 0}b")
254
  return {
255
  "ok": True,
256
- "content": doc_text
257
  }
258
 
259
 
 
235
 
236
 
237
  @tool
238
+ async def view_current_document() -> Dict[str, Any]:
239
  """
240
+ View the current state of the document being edited.
241
 
242
+ Use this tool to see the current document content after modifications.
243
+ This helps you verify previous edits and understand the current structure.
 
244
 
245
+ The document content is automatically provided by the workflow state.
246
 
247
  Returns:
248
  Dict with 'ok' (bool) and 'content' (str) containing the HTML document
249
  """
250
+ # doc_text is injected by _tools_node in doc_editor.py
251
+ logger.info(f" 🔍 view_current_document")
 
252
  return {
253
  "ok": True,
254
+ "content": ""
255
  }
256
 
257