Spaces:
Running
Running
Charles Grandjean commited on
Commit ·
e13ea3d
1
Parent(s): 96ab95b
doc editor revamp
Browse files- prompts/doc_editor.py +5 -7
- subagents/doc_editor.py +136 -211
- utils/doc_editor_tools.py +8 -10
prompts/doc_editor.py
CHANGED
|
@@ -88,26 +88,24 @@ Example:
|
|
| 88 |
}
|
| 89 |
```
|
| 90 |
|
| 91 |
-
###
|
| 92 |
-
View the current
|
| 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": "
|
| 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,
|
| 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
|
| 40 |
-
llm_tool_calling: LLM
|
| 41 |
"""
|
| 42 |
-
self.llm = llm
|
| 43 |
-
self.llm_tool_calling = llm_tool_calling
|
| 44 |
-
|
| 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 |
-
|
| 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"
|
| 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"
|
| 99 |
return "continue"
|
| 100 |
|
| 101 |
-
# Check if attempt_completion was ever called
|
| 102 |
-
attempt_completion_called =
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
break
|
| 107 |
|
| 108 |
if attempt_completion_called:
|
| 109 |
-
logger.info("
|
| 110 |
return "end"
|
| 111 |
|
| 112 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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"
|
| 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"
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 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"
|
| 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 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
result = await tool_func.ainvoke(args)
|
| 219 |
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 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("
|
| 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("
|
| 288 |
-
logger.info(f"
|
| 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("
|
| 326 |
logger.info("=" * 80)
|
| 327 |
-
logger.info(f"
|
| 328 |
-
logger.info(f"
|
| 329 |
-
logger.info(f"
|
| 330 |
-
logger.info(f"
|
| 331 |
-
logger.info(f"
|
| 332 |
if document_id:
|
| 333 |
-
logger.info(f"
|
| 334 |
if user_id:
|
| 335 |
-
logger.info(f"
|
| 336 |
|
| 337 |
if doc_summaries:
|
| 338 |
-
logger.info("
|
| 339 |
-
for i, summary in enumerate(doc_summaries
|
| 340 |
-
logger.info(f" [{i}] {str(summary)
|
| 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"
|
| 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("
|
| 362 |
|
| 363 |
-
# Run workflow
|
| 364 |
try:
|
| 365 |
-
logger.info("
|
| 366 |
final_state = await self.workflow.ainvoke(initial_state)
|
| 367 |
|
| 368 |
-
# Prepare result
|
| 369 |
final_summary = final_state.get("final_summary", "")
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
attempt_completion_called = True
|
| 375 |
-
break
|
| 376 |
success = attempt_completion_called
|
| 377 |
-
|
| 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("
|
| 386 |
logger.info("=" * 80)
|
| 387 |
-
logger.info(f"
|
| 388 |
-
logger.info(f"
|
| 389 |
-
logger.info(f"
|
| 390 |
-
logger.info(f"
|
| 391 |
-
logger.info(f"
|
| 392 |
|
| 393 |
-
# Log final document content
|
| 394 |
logger.info("=" * 80)
|
| 395 |
-
logger.info("
|
| 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("
|
| 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"
|
| 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("
|
| 425 |
logger.error("=" * 80)
|
| 426 |
-
logger.error(f"
|
| 427 |
-
logger.error("")
|
| 428 |
-
logger.error("
|
| 429 |
-
logger.error(f"
|
| 430 |
-
logger.error(f"
|
| 431 |
if document_id:
|
| 432 |
-
logger.error(f"
|
| 433 |
if user_id:
|
| 434 |
-
logger.error(f"
|
| 435 |
-
logger.error(f"
|
| 436 |
-
logger.error(f"
|
| 437 |
-
logger.error(f"
|
| 438 |
if plan:
|
| 439 |
-
logger.error(f"
|
| 440 |
-
logger.error("")
|
| 441 |
-
logger.error("
|
| 442 |
-
logger.error(f"
|
| 443 |
-
logger.error(f"
|
| 444 |
-
logger.error(f"
|
| 445 |
-
logger.error(f"
|
| 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,
|
| 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
|
| 239 |
"""
|
| 240 |
-
|
| 241 |
|
| 242 |
-
Use this tool to see the current document content
|
| 243 |
-
This
|
| 244 |
-
previous edits and understand the current structure.
|
| 245 |
|
| 246 |
-
The document content
|
| 247 |
|
| 248 |
Returns:
|
| 249 |
Dict with 'ok' (bool) and 'content' (str) containing the HTML document
|
| 250 |
"""
|
| 251 |
-
#
|
| 252 |
-
|
| 253 |
-
logger.info(f" 🔍 inspect_document | size:{len(doc_text) if doc_text else 0}b")
|
| 254 |
return {
|
| 255 |
"ok": True,
|
| 256 |
-
"content":
|
| 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 |
|