Charles Grandjean commited on
Commit
a8cf49b
·
1 Parent(s): da5a314

meta agent for doc

Browse files
agent_api.py CHANGED
@@ -25,7 +25,6 @@ from structured_outputs.api_models import (
25
  )
26
 
27
  from langgraph_agent import CyberLegalAgent
28
- from utils.conversation_manager import ConversationManager
29
  from utils.utils import validate_query
30
  from utils.lightrag_client import LightRAGClient
31
  from utils import tools
@@ -34,6 +33,7 @@ from subagents.lawyer_messenger import LawyerMessengerAgent
34
  from prompts.main import SYSTEM_PROMPT_CLIENT, SYSTEM_PROMPT_LAWYER
35
  from subagents.pdf_analyzer import PDFAnalyzerAgent
36
  from subagents.doc_editor import DocumentEditorAgent
 
37
  from langchain_openai import ChatOpenAI
38
  from mistralai import Mistral
39
  from langchain_google_genai import ChatGoogleGenerativeAI
@@ -181,7 +181,14 @@ class CyberLegalAPI:
181
  llm=self.llm_config.openai_llm,
182
  llm_tool_calling=self.llm_config.gemini_llm
183
  )
184
- self.conversation_manager = ConversationManager()
 
 
 
 
 
 
 
185
  logger.info(f"🔧 CyberLegalAPI initialized with {llm_provider.upper()} provider")
186
 
187
  def _format_documents_tree(self, node: TreeNode, indent: int = 0) -> str:
@@ -505,16 +512,15 @@ class CyberLegalAPI:
505
  else:
506
  logger.info("ℹ️ No conversation history provided")
507
 
508
- # Call document editor agent
509
  logger.info("=" * 80)
510
- logger.info("🤖 CALLING DOCUMENT EDITOR AGENT")
511
  logger.info("=" * 80)
512
- result = await self.doc_editor.edit_document(
513
  doc_text=doc_text,
514
  user_instruction=request.instruction,
515
  doc_summaries=doc_summaries,
516
  conversation_history=conversation_history,
517
- max_iterations=10,
518
  document_id=request.documentId,
519
  user_id=request.userId
520
  )
@@ -524,18 +530,17 @@ class CyberLegalAPI:
524
 
525
  # Log results
526
  logger.info("=" * 80)
527
- logger.info("📊 DOCUMENT EDITING RESULTS")
528
  logger.info("=" * 80)
529
  logger.info(f"✅ Success: {result['success']}")
530
- logger.info(f"🔄 Iterations: {result['iteration_count']}")
531
  logger.info(f"⏱️ Processing time: {processing_time:.2f}s")
532
  logger.info(f"💬 Message: {result['message'][:100]}...")
533
 
534
- # Prepare response - return HTML directly
535
- modified_document = result['doc_text'] if result['success'] else None
536
- if result['success']:
537
- logger.info(f"📏 Modified document size: {len(result['doc_text'])} bytes")
538
- logger.info(f"📈 Size change: {len(result['doc_text']) - len(doc_text):+d} bytes")
539
 
540
  response = DocCreatorResponse(
541
  response=result['message'],
 
25
  )
26
 
27
  from langgraph_agent import CyberLegalAgent
 
28
  from utils.utils import validate_query
29
  from utils.lightrag_client import LightRAGClient
30
  from utils import tools
 
33
  from prompts.main import SYSTEM_PROMPT_CLIENT, SYSTEM_PROMPT_LAWYER
34
  from subagents.pdf_analyzer import PDFAnalyzerAgent
35
  from subagents.doc_editor import DocumentEditorAgent
36
+ from subagents.doc_assistant import DocAssistant
37
  from langchain_openai import ChatOpenAI
38
  from mistralai import Mistral
39
  from langchain_google_genai import ChatGoogleGenerativeAI
 
181
  llm=self.llm_config.openai_llm,
182
  llm_tool_calling=self.llm_config.gemini_llm
183
  )
184
+ tools.doc_editor_agent = self.doc_editor
185
+ logger.info("✅ doc_editor_agent initialized globally")
186
+ # Initialize doc_assistant with Gemini for tool calling
187
+ self.doc_assistant = DocAssistant(
188
+ llm=self.llm_config.gemini_llm,
189
+ tools=tools.tools_for_doc,
190
+ tools_facade=tools.tools_for_doc_facade
191
+ )
192
  logger.info(f"🔧 CyberLegalAPI initialized with {llm_provider.upper()} provider")
193
 
194
  def _format_documents_tree(self, node: TreeNode, indent: int = 0) -> str:
 
512
  else:
513
  logger.info("ℹ️ No conversation history provided")
514
 
515
+ # Call doc_assistant (router agent that decides to respond or edit)
516
  logger.info("=" * 80)
517
+ logger.info("🤖 CALLING DOC ASSISTANT")
518
  logger.info("=" * 80)
519
+ result = await self.doc_assistant.process_request(
520
  doc_text=doc_text,
521
  user_instruction=request.instruction,
522
  doc_summaries=doc_summaries,
523
  conversation_history=conversation_history,
 
524
  document_id=request.documentId,
525
  user_id=request.userId
526
  )
 
530
 
531
  # Log results
532
  logger.info("=" * 80)
533
+ logger.info("📊 DOC ASSISTANT RESULTS")
534
  logger.info("=" * 80)
535
  logger.info(f"✅ Success: {result['success']}")
 
536
  logger.info(f"⏱️ Processing time: {processing_time:.2f}s")
537
  logger.info(f"💬 Message: {result['message'][:100]}...")
538
 
539
+ # Prepare response - doc_assistant returns modified_document or None
540
+ modified_document = result.get('modified_document')
541
+ if modified_document:
542
+ logger.info(f"📏 Modified document size: {len(modified_document)} bytes")
543
+ logger.info(f"📈 Size change: {len(modified_document) - len(doc_text):+d} bytes")
544
 
545
  response = DocCreatorResponse(
546
  response=result['message'],
agent_states/doc_assistant_state.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ State for DocCreatorRouterAgent
4
+ Simple state for the router that decides whether to edit or just respond
5
+ """
6
+
7
+ from typing import TypedDict, List, Dict, Any, Optional
8
+ from langchain_core.messages import BaseMessage
9
+
10
+
11
+ class DocAssistantState(TypedDict):
12
+ """
13
+ State for the document creator router agent.
14
+
15
+ This agent decides whether to:
16
+ 1. Just respond to the user (no document changes needed)
17
+ 2. Call the edit_document tool to modify the document
18
+
19
+ Simple single-pass workflow with optional tool calling.
20
+ """
21
+ doc_text: str # The HTML document content
22
+ user_instruction: str # User's instruction or question
23
+ doc_summaries: Optional[List[str]] # Optional context from other documents
24
+ conversation_history: Optional[List[Dict[str, str]]] # Optional conversation history
25
+
26
+ # Agent response
27
+ message: Optional[str] # Agent's response to the user
28
+
29
+ # Modified document (if editing was performed)
30
+ modified_document: Optional[str] # The modified document, if edit_document was called
31
+
32
+ # Tool execution results
33
+ intermediate_steps: List[BaseMessage] # Conversation history including tool calls
34
+
35
+ # Metadata
36
+ document_id: Optional[str] # Optional UUID of the document for live updates
37
+ user_id: Optional[str] # Optional user ID for authentication
agent_states/doc_editor_state.py CHANGED
@@ -19,6 +19,9 @@ class DocEditorState(TypedDict):
19
  # User instruction
20
  user_instruction: str
21
 
 
 
 
22
  # Workflow control
23
  iteration_count: int
24
  max_iterations: int
 
19
  # User instruction
20
  user_instruction: str
21
 
22
+ # Execution plan (optional, provided by DocAssistant)
23
+ plan: Optional[str]
24
+
25
  # Workflow control
26
  iteration_count: int
27
  max_iterations: int
prompts/doc_assistant.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ System prompts for the doc creator router agent
4
+ """
5
+
6
+ ROUTER_SYSTEM_PROMPT = """You are a Document Router Agent that decides whether to respond to a user's question or modify their HTML document.
7
+
8
+ ## CRITICAL RULES
9
+
10
+ 1. **DECIDE FIRST**: Determine if the user wants to UNDERSTAND the document or MODIFY it.
11
+
12
+ 2. **UNDERSTAND = RESPOND**: If the user asks questions, wants explanations, or requests information → Just respond with your answer. No tool call needed.
13
+
14
+ 3. **MODIFY = USE TOOL**: If the user wants to change, add, remove, or restructure content → Use the edit_document tool.
15
+
16
+ 4. **DOCUMENT IS IN CONTEXT**: The current HTML document is included in this message. You can reference it directly.
17
+
18
+ 5. **ONE DECISION**: Make a clear decision. Either respond or call a tool. Don't hesitate.
19
+
20
+ 6. **BE PRECISE**: If you use edit_document, pass the user's exact instruction.
21
+
22
+ 7. **ACCESS OTHER DOCUMENTS**: You can use retrieve_lawyer_document if you need to reference other documents from the lawyer's database.
23
+
24
+ ## WHEN TO JUST RESPOND
25
+
26
+ Respond directly without tool calls when the user wants to:
27
+
28
+ - **Understand content**: "What does this contract say about duration?"
29
+ - **Explain sections**: "Explain Article 3 in simple terms"
30
+ - **Extract information**: "What are the key obligations in this agreement?"
31
+ - **Summarize**: "Give me a summary of this document"
32
+ - **Clarify meaning**: "What does this clause mean?"
33
+ - **Answer questions**: "Is there a termination clause?"
34
+ - **Compare terms**: "What's the difference between Article 1 and Article 2?"
35
+
36
+ ## WHEN TO CALL edit_document
37
+
38
+ Use the edit_document tool when the user wants to:
39
+
40
+ - **Change text**: "Change 12 months to 24 months"
41
+ - **Add content**: "Add a GDPR compliance clause"
42
+ - **Remove content**: "Delete the section about confidentiality"
43
+ - **Restructure**: "Move Article 3 after Article 1"
44
+ - **Insert sections**: "Add Article 4 about pricing after Article 3"
45
+ - **Modify clauses**: "Update the liability clause to cap at $10,000"
46
+ - **Update values**: "Change the price to $500"
47
+ - **Rephrase**: "Rewrite this paragraph to be more formal"
48
+
49
+ ## AVAILABLE TOOLS
50
+
51
+ ### edit_document
52
+ Modify the HTML document by providing a detailed execution plan. The instruction will be provided automatically when the tool is triggered.
53
+
54
+ Parameters:
55
+ - plan: A numbered list of up to 6 steps describing exactly how to execute the modification
56
+ - doc_summaries: Optional context from other documents (for the editor agent)
57
+ - conversation_history: Optional conversation history (for the editor agent)
58
+
59
+ The plan should be specific and actionable, describing exactly what HTML elements to target and what changes to make.
60
+
61
+ Example:
62
+ ```json
63
+ {
64
+ "type": "tool_call",
65
+ "name": "edit_document",
66
+ "arguments": {
67
+ "plan": "1. Find the paragraph containing '12 months'\n2. Replace '12 months' with '24 months'\n3. Verify the change is correct"
68
+ }
69
+ }
70
+ ```
71
+
72
+ Complex example with multiple steps:
73
+ ```json
74
+ {
75
+ "type": "tool_call",
76
+ "name": "edit_document",
77
+ "arguments": {
78
+ "plan": "1. Locate the Article 3 heading\n2. Add a new heading 'Article 4 - GDPR Compliance' after Article 3\n3. Add a GDPR clause paragraph under the new heading\n4. Find the data processing section in the document\n5. Update the data processing paragraph to include GDPR references\n6. Verify all changes maintain valid HTML structure"
79
+ }
80
+ }
81
+ ```
82
+
83
+ ### retrieve_lawyer_document
84
+ Retrieve a specific document from the lawyer's document database for additional context.
85
+
86
+ Parameters:
87
+ - user_id: The lawyer's user ID
88
+ - file_path: Path to the document (e.g., "Contracts/bail-commercial.pdf")
89
+
90
+ Example:
91
+ ```json
92
+ {
93
+ "type": "tool_call",
94
+ "name": "retrieve_lawyer_document",
95
+ "arguments": {
96
+ "user_id": "user-uuid",
97
+ "file_path": "Contracts/contract-template.pdf"
98
+ }
99
+ }
100
+ ```
101
+
102
+ ## DECISION PROCESS
103
+
104
+ 1. **Analyze the user's request**
105
+ 2. **Ask yourself**: Does this require CHANGING the document, or just UNDERSTANDING it?
106
+ 3. **If understanding → Respond directly**
107
+ 4. **If modifying → Call edit_document with the exact instruction**
108
+ 5. **If you need other documents → Call retrieve_lawyer_document first, then proceed**
109
+
110
+ ## WORKFLOW EXAMPLES
111
+
112
+ ### Example 1: Understanding (Just Respond)
113
+ User: "What does Article 2 say about termination?"
114
+ → Analyze: User wants to understand, not modify
115
+ → Respond: "Article 2 states that either party may terminate with 30 days notice..."
116
+
117
+ ### Example 2: Modification (Use Tool)
118
+ User: "Change 12 months to 24 months"
119
+ → Analyze: User wants to modify the document
120
+ → Call edit_document with instruction: "Change 12 months to 24 months"
121
+
122
+ ### Example 3: Complex Modification
123
+ User: "Add a GDPR compliance clause after Article 3"
124
+ → Analyze: User wants to add content
125
+ → Call edit_document with instruction: "Add a GDPR compliance clause after Article 3"
126
+
127
+ ### Example 4: Question + Modification
128
+ User: "Is there a penalty clause? If not, add one."
129
+ → Analyze: Two parts - understand then modify
130
+ → Respond: "No, there is no penalty clause. I'll add one for you."
131
+ → Call edit_document with instruction: "Add a penalty clause"
132
+
133
+ ### Example 5: Cross-Document Reference
134
+ User: "Compare this contract's termination clause with the one in Contracts/termination-standard.pdf"
135
+ → Analyze: Need to reference another document
136
+ → Call retrieve_lawyer_document for the other document
137
+ → Compare and respond
138
+
139
+ ## IMPORTANT NOTES
140
+
141
+ - **Document is provided**: The HTML document is in the message. Read it carefully.
142
+ - **Context available**: Document summaries and conversation history are provided if available.
143
+ - **No iteration needed**: You make one decision. If you call edit_document, it handles the full editing process.
144
+ - **Be helpful**: Whether responding or editing, provide clear, useful information.
145
+ - **Ask if unclear**: If the user's request is ambiguous, ask for clarification.
146
+
147
+ Remember: Your job is to be efficient and helpful. If no changes are needed, just answer. If changes are needed, use the tool.
148
+ """
149
+
150
+
151
+ def get_router_system_prompt() -> str:
152
+ """Get the system prompt for the doc creator router agent."""
153
+ return ROUTER_SYSTEM_PROMPT
154
+
155
+
156
+ def get_router_user_prompt(doc_text: str, instruction: str, doc_summaries: list = [], conversation_history: list = []) -> str:
157
+ """
158
+ Build the user prompt for the router agent.
159
+
160
+ Args:
161
+ doc_text: The HTML document content
162
+ instruction: User's instruction or question
163
+ doc_summaries: Optional list of document summaries for context
164
+ conversation_history: Optional conversation history
165
+
166
+ Returns:
167
+ Formatted user prompt
168
+ """
169
+ prompt_parts = []
170
+
171
+ # Add document
172
+ prompt_parts.append("## Current Document (HTML)")
173
+ prompt_parts.append(doc_text)
174
+ prompt_parts.append("")
175
+
176
+ # Add document summaries if provided
177
+ if doc_summaries:
178
+ prompt_parts.append("## Related Documents (for context)")
179
+ for summary in doc_summaries:
180
+ prompt_parts.append(f"- {summary}")
181
+ prompt_parts.append("")
182
+
183
+ # Add conversation history if provided
184
+ if conversation_history:
185
+ prompt_parts.append("## Previous Conversation")
186
+ for msg in conversation_history:
187
+ role = msg.get("role", "").capitalize()
188
+ content = msg.get("content", "")
189
+ prompt_parts.append(f"{role}: {content}")
190
+ prompt_parts.append("")
191
+
192
+ # Add user instruction
193
+ prompt_parts.append("## User Request")
194
+ prompt_parts.append(instruction)
195
+
196
+ return "\n".join(prompt_parts)
subagents/doc_assistant.py ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ DocAssistant - LangGraph agent that routes between responding and editing
4
+ Single node agent that decides whether to respond to user questions or edit the document
5
+ """
6
+
7
+ import logging
8
+ import traceback
9
+ from typing import Dict, Any, List, Optional
10
+ from langgraph.graph import StateGraph, END
11
+ from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage
12
+
13
+ from utils.tools import _edit_document
14
+ from agent_states.doc_assistant_state import DocAssistantState
15
+ from prompts.doc_assistant import get_router_system_prompt, get_router_user_prompt
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class DocAssistant:
21
+ """
22
+ Router agent that decides whether to respond to user questions or edit the document.
23
+
24
+ Workflow:
25
+ - Single node with optional tool calling
26
+ - Document is in user prompt (no inspect needed)
27
+ - Agent decides: respond directly OR call edit_document tool
28
+ - No looping at router level (single pass)
29
+ """
30
+
31
+ def __init__(self, llm, tools: List[Any] = None, tools_facade: List[Any] = None):
32
+ """
33
+ Initialize the router agent.
34
+
35
+ Args:
36
+ llm: LLM for routing decisions (with tool calling capability)
37
+ tools: Real tool implementations for lookup
38
+ tools_facade: Facade tools for LLM (minimal parameters)
39
+ """
40
+ self.tools = tools
41
+ self.tools_facade = tools_facade if tools_facade else tools
42
+
43
+ # Bind facade tools to LLM - optional tool calling
44
+ self.llm_with_tools = self.llm.bind_tools(self.tools_facade)
45
+
46
+ logger.info("🔧 DocAssistant initialized")
47
+ logger.info(f"🤖 Using {type(llm).__name__} for routing decisions")
48
+ logger.info(f"🛠️ Tools available: {[t.name for t in self.tools_facade]}")
49
+
50
+ self.workflow = self._build_workflow()
51
+
52
+ def _build_workflow(self) -> StateGraph:
53
+ """Build the LangGraph workflow for the router agent."""
54
+ workflow = StateGraph(DocAssistantState)
55
+ workflow.add_node("agent", self._agent_node)
56
+ workflow.add_node("tools", self._tools_node)
57
+ workflow.set_entry_point("agent")
58
+
59
+ # Conditional edge after agent: go to tools if tool calls, else END
60
+ workflow.add_conditional_edges(
61
+ "agent",
62
+ self._should_call_tools,
63
+ {"tools": "tools", "end": END}
64
+ )
65
+
66
+ # Conditional edge after tools: END if edit_document was called, else continue
67
+ workflow.add_conditional_edges(
68
+ "tools",
69
+ self._after_tools,
70
+ {"end": END, "continue": "agent"}
71
+ )
72
+
73
+ return workflow.compile()
74
+
75
+ def _should_call_tools(self, state: DocAssistantState) -> str:
76
+ """
77
+ Decide whether to call tools after agent node.
78
+
79
+ Returns:
80
+ "tools" if agent made tool calls, "end" otherwise
81
+ """
82
+ intermediate_steps = state.get("intermediate_steps", [])
83
+ if not intermediate_steps:
84
+ return "end"
85
+
86
+ last_message = intermediate_steps[-1]
87
+
88
+ # Check if last message has tool calls
89
+ if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
90
+ tool_names = [tc['name'] for tc in last_message.tool_calls]
91
+ logger.info(f"🔧 Agent calling tools: {tool_names}")
92
+ return "tools"
93
+
94
+ return "end"
95
+
96
+ async def _agent_node(self, state: DocAssistantState) -> DocAssistantState:
97
+ """Agent node: Generate response or tool call based on user request."""
98
+ intermediate_steps = state.get("intermediate_steps", [])
99
+
100
+ logger.info("🎯 Router agent processing request...")
101
+
102
+ # Build messages
103
+ messages = [
104
+ SystemMessage(content=get_router_system_prompt()),
105
+ HumanMessage(content=get_router_user_prompt(
106
+ doc_text=state["doc_text"],
107
+ instruction=state["user_instruction"],
108
+ doc_summaries=state.get("doc_summaries", []),
109
+ conversation_history=state.get("conversation_history", [])
110
+ ))
111
+ ]
112
+
113
+ logger.info(f"📏 Document size: {len(state['doc_text'])} bytes")
114
+ logger.info(f"📋 Instruction: {state['user_instruction'][:100]}{'...' if len(state['user_instruction']) > 100 else ''}")
115
+
116
+ # Call LLM
117
+ response = await self.llm_with_tools.ainvoke(messages)
118
+ intermediate_steps.append(response)
119
+
120
+ logger.info(f"📊 Response type: {type(response).__name__}")
121
+ if hasattr(response, 'tool_calls') and response.tool_calls:
122
+ logger.info(f"🔧 Tool calls: {[tc['name'] for tc in response.tool_calls]}")
123
+ else:
124
+ logger.info("💬 Direct response (no tool calls)")
125
+
126
+ state["intermediate_steps"] = intermediate_steps
127
+ return state
128
+
129
+ async def _tools_node(self, state: DocAssistantState) -> DocAssistantState:
130
+ """Tools node: Execute tool calls (edit_document or retrieve_lawyer_document)."""
131
+ intermediate_steps = state.get("intermediate_steps", [])
132
+ last_message = intermediate_steps[-1]
133
+
134
+ if not (hasattr(last_message, 'tool_calls') and last_message.tool_calls):
135
+ return state
136
+
137
+ for tool_call in last_message.tool_calls:
138
+ tool_name = tool_call['name']
139
+ args = tool_call['args'].copy()
140
+
141
+ # Get the tool function directly from self.tools
142
+ tool_func = next((t for t in self.tools if t.name == tool_name), None)
143
+
144
+ if tool_func:
145
+ try:
146
+ # Special handling for edit_document - inject all necessary parameters
147
+ if tool_name == "edit_document":
148
+ logger.info("📝 edit_document tool called - invoking doc_editor_agent")
149
+
150
+ # Call the real implementation with all parameters
151
+ result = await _edit_document(
152
+ doc_text=state["doc_text"],
153
+ user_instruction=state["user_instruction"],
154
+ plan=args.get("plan", ""),
155
+ doc_summaries=state.get("doc_summaries", []),
156
+ conversation_history=state.get("conversation_history", []),
157
+ max_iterations=10,
158
+ document_id=state.get("document_id"),
159
+ user_id=state.get("user_id")
160
+ )
161
+
162
+ # Update state with result
163
+ if result.get("ok") and result.get("doc_text"):
164
+ state["modified_document"] = result["doc_text"]
165
+
166
+ # Add result as tool message
167
+ intermediate_steps.append(
168
+ ToolMessage(
169
+ content=result.get("message", "Document editing completed"),
170
+ tool_call_id=tool_call['id'],
171
+ name=tool_name
172
+ )
173
+ )
174
+
175
+ logger.info(f"✅ edit_document completed: {result.get('success', False)}")
176
+
177
+ # Special handling for retrieve_lawyer_document - inject user_id
178
+ elif tool_name == "retrieve_lawyer_document":
179
+ from utils.tools import _retrieve_lawyer_document
180
+ logger.info(f"📄 retrieve_lawyer_document tool called: {args.get('file_path')}")
181
+
182
+ # Inject user_id if available
183
+ if "user_id" not in args and state.get("user_id"):
184
+ args["user_id"] = state["user_id"]
185
+
186
+ result = await _retrieve_lawyer_document(**args)
187
+
188
+ intermediate_steps.append(
189
+ ToolMessage(
190
+ content=result,
191
+ tool_call_id=tool_call['id'],
192
+ name=tool_name
193
+ )
194
+ )
195
+
196
+ logger.info(f"✅ retrieve_lawyer_document completed")
197
+
198
+ # Special handling for query_knowledge_graph
199
+ elif tool_name == "query_knowledge_graph":
200
+ from utils.tools import _query_knowledge_graph
201
+ logger.info(f"🔍 query_knowledge_graph tool called: {args.get('query')}")
202
+
203
+ result = await _query_knowledge_graph(**args)
204
+
205
+ intermediate_steps.append(
206
+ ToolMessage(
207
+ content=result,
208
+ tool_call_id=tool_call['id'],
209
+ name=tool_name
210
+ )
211
+ )
212
+
213
+ logger.info(f"✅ query_knowledge_graph completed")
214
+
215
+ # Generic tool invocation (shouldn't happen with current tools)
216
+ else:
217
+ result = await tool_func.ainvoke(args)
218
+ intermediate_steps.append(
219
+ ToolMessage(
220
+ content=str(result),
221
+ tool_call_id=tool_call['id'],
222
+ name=tool_name
223
+ )
224
+ )
225
+
226
+ except Exception as e:
227
+ logger.error(f"❌ Error executing {tool_name}: {str(e)}")
228
+ intermediate_steps.append(
229
+ ToolMessage(
230
+ content=f"Error: {str(e)}",
231
+ tool_call_id=tool_call['id'],
232
+ name=tool_name
233
+ )
234
+ )
235
+ else:
236
+ logger.warning(f"⚠️ Tool function not found for {tool_name}")
237
+
238
+ state["intermediate_steps"] = intermediate_steps
239
+ return state
240
+
241
+ def _after_tools(self, state: DocAssistantState) -> str:
242
+ """
243
+ Decide whether to continue after tools node.
244
+
245
+ Returns:
246
+ "end" if edit_document was called (stops workflow),
247
+ "continue" if retrieve_lawyer_document or query_knowledge_graph were called (allows more tool calls or response)
248
+ """
249
+ intermediate_steps = state.get("intermediate_steps", [])
250
+
251
+ # Check if edit_document was called
252
+ for msg in reversed(intermediate_steps):
253
+ if isinstance(msg, ToolMessage) and msg.name == "edit_document":
254
+ logger.info("✅ edit_document called - ending router workflow")
255
+ return "end"
256
+
257
+ # If edit_document wasn't called, continue (allows agent to make more tool calls or respond)
258
+ logger.info("🔄 Continuing router workflow (edit_document not yet called)")
259
+ return "continue"
260
+
261
+ async def process_request(
262
+ self,
263
+ doc_text: str,
264
+ user_instruction: str,
265
+ doc_summaries: List[str] = [],
266
+ conversation_history: List[Dict[str, str]] = [],
267
+ document_id: Optional[str] = None,
268
+ user_id: Optional[str] = None
269
+ ) -> Dict[str, Any]:
270
+ """
271
+ Process the user's request and decide whether to respond or edit.
272
+
273
+ Args:
274
+ doc_text: The HTML document content
275
+ user_instruction: User's instruction or question
276
+ doc_summaries: Optional list of document summaries for context
277
+ conversation_history: Optional conversation history
278
+ document_id: Optional UUID of the document for live updates
279
+ user_id: Optional user ID for authentication
280
+
281
+ Returns:
282
+ Dict with:
283
+ - message: Response message to the user
284
+ - modified_document: Modified document (if editing was done) or None
285
+ - success: Boolean indicating success
286
+ """
287
+ logger.info("=" * 80)
288
+ logger.info("🎯 DOC ASSISTANT STARTING")
289
+ logger.info("=" * 80)
290
+ logger.info(f"📏 Document size: {len(doc_text)} bytes")
291
+ logger.info(f"📋 Instruction: {user_instruction[:100]}{'...' if len(user_instruction) > 100 else ''}")
292
+ logger.info(f"📚 Document summaries: {len(doc_summaries)}")
293
+ logger.info(f"💬 Conversation history: {len(conversation_history)} messages")
294
+
295
+ try:
296
+ # Initialize state
297
+ initial_state = {
298
+ "doc_text": doc_text,
299
+ "doc_summaries": doc_summaries,
300
+ "conversation_history": conversation_history,
301
+ "user_instruction": user_instruction,
302
+ "intermediate_steps": [],
303
+ "document_id": document_id,
304
+ "user_id": user_id,
305
+ "modified_document": None
306
+ }
307
+
308
+ # Run workflow
309
+ logger.info("🔄 Invoking router workflow...")
310
+ final_state = await self.workflow.ainvoke(initial_state)
311
+
312
+ # Determine result
313
+ intermediate_steps = final_state.get("intermediate_steps", [])
314
+
315
+ # Check if edit_document was called successfully
316
+ if final_state.get("modified_document"):
317
+ logger.info("✅ Document was modified")
318
+ # Find the tool message with the result
319
+ for msg in intermediate_steps:
320
+ if isinstance(msg, ToolMessage) and msg.name == "edit_document":
321
+ return {
322
+ "message": msg.content,
323
+ "modified_document": final_state["modified_document"],
324
+ "success": True
325
+ }
326
+ else:
327
+ # No modification - just a response
328
+ logger.info("💬 Router responded without editing")
329
+ # Find the last AIMessage (agent's response)
330
+ for msg in reversed(intermediate_steps):
331
+ if isinstance(msg, AIMessage) and not (hasattr(msg, 'tool_calls') and msg.tool_calls):
332
+ return {
333
+ "message": msg.content,
334
+ "modified_document": None,
335
+ "success": True
336
+ }
337
+
338
+ # Fallback
339
+ return {
340
+ "message": "Processing completed",
341
+ "modified_document": None,
342
+ "success": True
343
+ }
344
+
345
+ except Exception as e:
346
+ logger.error("=" * 80)
347
+ logger.error("❌ DOC ASSISTANT FAILED")
348
+ logger.error("=" * 80)
349
+ logger.error(f"❌ Error: {str(e)}")
350
+ logger.error(f"🔍 Traceback: {traceback.format_exc()}")
351
+ return {
352
+ "message": f"Error processing request: {str(e)}",
353
+ "modified_document": None,
354
+ "success": False
355
+ }
subagents/doc_editor.py CHANGED
@@ -165,12 +165,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
  # Add document and instruction
170
  full_message = (
171
  f"Current document (HTML):\n{state['doc_text']}\n\n"
172
  f"{context_msg}\n\n"
173
  f"Instruction: {state['user_instruction']}"
 
174
  )
175
  intermediate_steps.append(HumanMessage(content=full_message))
176
  logger.info(f"📚 Context: {len(conversation_history)} hist + {len(doc_summaries)} summaries")
@@ -179,8 +183,6 @@ class DocumentEditorAgent:
179
  response = await self.llm_with_tools.ainvoke(intermediate_steps)
180
  intermediate_steps.append(response)
181
 
182
- # Increment iteration count for this LLM call (regardless of tool calls)
183
- state["iteration_count"] = iteration_count + 1
184
  state["intermediate_steps"] = intermediate_steps
185
  return state
186
 
@@ -291,6 +293,7 @@ Full conversation history (including all tool calls and results):
291
  self,
292
  doc_text: str,
293
  user_instruction: str,
 
294
  doc_summaries: List[str] = [],
295
  conversation_history: List[Dict[str, str]] = [],
296
  max_iterations: int = 10,
@@ -303,6 +306,7 @@ Full conversation history (including all tool calls and results):
303
  Args:
304
  doc_text: HTML document string
305
  user_instruction: What changes to make to the document
 
306
  doc_summaries: Optional summaries of the document for context
307
  conversation_history: Optional previous conversation messages for context
308
  max_iterations: Maximum number of edit iterations (default: 10)
@@ -344,6 +348,7 @@ Full conversation history (including all tool calls and results):
344
  initial_state = {
345
  "doc_text": doc_text,
346
  "doc_summaries": doc_summaries,
 
347
  "conversation_history": conversation_history,
348
  "user_instruction": user_instruction,
349
  "iteration_count": 0,
 
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")
 
183
  response = await self.llm_with_tools.ainvoke(intermediate_steps)
184
  intermediate_steps.append(response)
185
 
 
 
186
  state["intermediate_steps"] = intermediate_steps
187
  return state
188
 
 
293
  self,
294
  doc_text: str,
295
  user_instruction: str,
296
+ plan: Optional[str] = None,
297
  doc_summaries: List[str] = [],
298
  conversation_history: List[Dict[str, str]] = [],
299
  max_iterations: int = 10,
 
306
  Args:
307
  doc_text: HTML document string
308
  user_instruction: What changes to make to the document
309
+ plan: Optional execution plan provided by DocAssistant
310
  doc_summaries: Optional summaries of the document for context
311
  conversation_history: Optional previous conversation messages for context
312
  max_iterations: Maximum number of edit iterations (default: 10)
 
348
  initial_state = {
349
  "doc_text": doc_text,
350
  "doc_summaries": doc_summaries,
351
+ "plan": plan,
352
  "conversation_history": conversation_history,
353
  "user_instruction": user_instruction,
354
  "iteration_count": 0,
utils/conversation_manager.py DELETED
@@ -1,129 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Conversation management for the agent
4
- """
5
-
6
- from typing import Dict, List, Any
7
- from datetime import datetime
8
-
9
-
10
- class ConversationManager:
11
- """
12
- Manages conversation history and context
13
- """
14
-
15
- def __init__(self, max_history: int = 10):
16
- self.max_history = max_history
17
-
18
- def add_exchange(self, history: List[Dict[str, str]], user_query: str, agent_response: str) -> List[Dict[str, str]]:
19
- """
20
- Add a new user-agent exchange to the conversation history
21
- """
22
- updated_history = history.copy()
23
-
24
- # Add user message
25
- updated_history.append({
26
- "role": "user",
27
- "content": user_query,
28
- "timestamp": datetime.now().isoformat()
29
- })
30
-
31
- # Add agent response
32
- updated_history.append({
33
- "role": "assistant",
34
- "content": agent_response,
35
- "timestamp": datetime.now().isoformat()
36
- })
37
-
38
- # Keep only the last max_history exchanges (pairs)
39
- if len(updated_history) > self.max_history * 2:
40
- updated_history = updated_history[-self.max_history * 2:]
41
-
42
- return updated_history
43
-
44
- def format_for_lightrag(self, history: List[Dict[str, str]]) -> List[Dict[str, str]]:
45
- """
46
- Format conversation history for LightRAG API
47
- """
48
- formatted = []
49
- for exchange in history:
50
- formatted.append({
51
- "role": exchange["role"],
52
- "content": exchange["content"]
53
- })
54
- return formatted
55
-
56
- def get_context_summary(self, history: List[Dict[str, str]]) -> str:
57
- """
58
- Generate a summary of recent conversation context
59
- """
60
- if not history:
61
- return "No previous conversation context."
62
-
63
- recent_exchanges = history[-6:] # Last 3 exchanges
64
- context_parts = []
65
-
66
- for i, exchange in enumerate(recent_exchanges):
67
- role = "User" if exchange["role"] == "user" else "Assistant"
68
- context_parts.append(f"{role}: {exchange['content']}")
69
-
70
- return "\n".join(context_parts)
71
-
72
-
73
- class ConversationFormatter:
74
- """
75
- Format conversation data for different purposes
76
- """
77
-
78
- @staticmethod
79
- def build_conversation_history(history: List[Dict[str, str]], max_turns: int = 10) -> List[Dict[str, str]]:
80
- """
81
- Build conversation history for LightRAG API
82
- """
83
- if not history:
84
- return []
85
-
86
- # Take last max_turns pairs (user + assistant)
87
- recent_history = history[-max_turns*2:]
88
- formatted = []
89
-
90
- for exchange in recent_history:
91
- # Handle both Message objects and dictionary formats
92
- if hasattr(exchange, 'role'):
93
- role = exchange.role
94
- content = exchange.content
95
- else:
96
- role = exchange["role"]
97
- content = exchange["content"]
98
-
99
- formatted.append({
100
- "role": role,
101
- "content": content
102
- })
103
-
104
- return formatted
105
-
106
- @staticmethod
107
- def create_context_summary(history: List[Dict[str, str]]) -> str:
108
- """
109
- Create a summary of conversation context
110
- """
111
- if not history:
112
- return "No previous conversation."
113
-
114
- recent_exchanges = history[-4:] # Last 2 exchanges
115
- context_parts = []
116
-
117
- for exchange in recent_exchanges:
118
- # Handle both Message objects and dictionary formats
119
- if hasattr(exchange, 'role'):
120
- role = "User" if exchange.role == "user" else "Assistant"
121
- content = exchange.content
122
- else:
123
- role = "User" if exchange["role"] == "user" else "Assistant"
124
- content = exchange["content"]
125
-
126
- content = content[:100] + "..." if len(content) > 100 else content
127
- context_parts.append(f"{role}: {content}")
128
-
129
- return "\n".join(context_parts)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
utils/lightrag_client.py CHANGED
@@ -165,50 +165,6 @@ class LightRAGClient:
165
  return ref_list
166
 
167
 
168
- class ResponseProcessor:
169
- """
170
- Process and enhance LightRAG responses
171
- """
172
-
173
- @staticmethod
174
- def extract_main_content(response: Dict[str, Any]) -> str:
175
- """
176
- Extract the main response content
177
- """
178
- return response.get("response", "No response available.")
179
-
180
- @staticmethod
181
- def format_references(references: List[str]) -> str:
182
- """
183
- Format reference list for display
184
- """
185
- if not references:
186
- return ""
187
-
188
- ref_text = "\n\n**📚 References:**\n"
189
- for ref in references:
190
- ref_text += f"• {ref}\n"
191
-
192
- return ref_text
193
-
194
- @staticmethod
195
- def extract_key_entities(response: Dict[str, Any]) -> List[str]:
196
- """
197
- Extract key entities mentioned in the response
198
- """
199
- # This could be enhanced if LightRAG provides entity information
200
- content = response.get("response", "")
201
-
202
- # Simple entity extraction based on common legal terms
203
- legal_entities = []
204
- regulations = ["GDPR", "NIS2", "DORA", "CRA", "eIDAS", "Cyber Resilience Act"]
205
-
206
- for reg in regulations:
207
- if reg.lower() in content.lower():
208
- legal_entities.append(reg)
209
-
210
- return list(set(legal_entities)) # Remove duplicates
211
-
212
 
213
  _lightrag_client_cache: Dict[str, LightRAGClient] = {}
214
 
 
165
  return ref_list
166
 
167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
  _lightrag_client_cache: Dict[str, LightRAGClient] = {}
170
 
utils/tools.py CHANGED
@@ -17,10 +17,10 @@ import resend
17
  # Global instances - will be initialized in agent_api.py
18
  lawyer_selector_agent: Optional[LawyerSelectorAgent] = None
19
  lawyer_messenger_agent: Optional[LawyerMessengerAgent] = None
20
- doc_editor_agent: Optional[DocumentEditorAgent] = None
21
  lightrag_client: Optional[LightRAGClient] = None
22
- tavily_search = None
23
  resend_api_key: Optional[str] = None
 
24
 
25
  @tool
26
  async def query_knowledge_graph(
@@ -249,96 +249,6 @@ async def _message_lawyer(conversation_history, user_id) -> str:
249
  except Exception as e:
250
  return f"Error sending message to lawyer: {str(e)}"
251
 
252
-
253
- @tool
254
- async def edit_document() -> str:
255
- """
256
- Edit a TipTap JSON document according to user instructions.
257
-
258
- This tool uses an AI agent to intelligently modify documents by making
259
- precise edits (replace, add, delete) while maintaining valid JSON structure.
260
- The agent works iteratively, validating each edit before proceeding.
261
-
262
- Use this tool when the user wants to modify a document, such as:
263
- - Changing text content (e.g., "Change 12 months to 24 months")
264
- - Adding new sections or clauses
265
- - Removing unwanted content
266
- - Restructuring parts of the document
267
-
268
- Args:
269
- doc_text: The TipTap JSON document as a string (must be canonical format)
270
- instruction: What changes to make to the document
271
- doc_summaries: Optional list of document summaries for context
272
-
273
- Returns:
274
- A summary of what was changed and the modified document
275
- """
276
- try:
277
- if doc_editor_agent is None:
278
- raise ValueError("DocumentEditorAgent not initialized. Please initialize it in agent_api.py")
279
- raise ValueError("doc_text and instruction not provided - these should be injected by the agent")
280
- except Exception as e:
281
- return f"Error editing document: {str(e)}"
282
-
283
-
284
- @tool
285
- async def _edit_document(doc_text: str, instruction: str, doc_summaries: List[str] = []) -> str:
286
- """
287
- Edit a TipTap JSON document according to user instructions.
288
-
289
- This tool uses an AI agent to intelligently modify documents by making
290
- precise edits (replace, add, delete) while maintaining valid JSON structure.
291
- The agent works iteratively, validating each edit before proceeding.
292
-
293
- Use this tool when the user wants to modify a document, such as:
294
- - Changing text content (e.g., "Change 12 months to 24 months")
295
- - Adding new sections or clauses
296
- - Removing unwanted content
297
- - Restructuring parts of the document
298
-
299
- Args:
300
- doc_text: The TipTap JSON document as a string (must be canonical format with sort_keys=True, indent=2)
301
- instruction: What changes to make to the document
302
- doc_summaries: Optional list of document summaries for context
303
-
304
- Returns:
305
- A summary of what was changed and the modified document
306
- """
307
- try:
308
- if doc_editor_agent is None:
309
- raise ValueError("DocumentEditorAgent not initialized. Please initialize it in agent_api.py")
310
-
311
- # Ensure doc_text is canonical format
312
- try:
313
- doc_obj = json.loads(doc_text)
314
- canonical_doc_text = json.dumps(doc_obj, ensure_ascii=False, sort_keys=True, indent=2)
315
- except json.JSONDecodeError:
316
- return f"Error: Invalid JSON provided in doc_text"
317
-
318
- # Run the document editor agent
319
- result = await doc_editor_agent.edit_document(
320
- doc_text=canonical_doc_text,
321
- user_instruction=instruction,
322
- doc_summaries=doc_summaries
323
- )
324
-
325
- if result["success"]:
326
- output = [
327
- "✅ Document editing completed successfully",
328
- "=" * 80,
329
- f"\nSummary: {result['message']}",
330
- f"\nIterations: {result['iteration_count']}",
331
- "\nModified document (TipTap JSON):",
332
- result["doc_text"]
333
- ]
334
- return "\n".join(output)
335
- else:
336
- return f"❌ Failed to edit document: {result['message']}"
337
-
338
- except Exception as e:
339
- return f"Error editing document: {str(e)}"
340
-
341
-
342
  @tool
343
  async def retrieve_lawyer_document(file_path: str) -> str:
344
  """
@@ -447,9 +357,93 @@ async def _retrieve_lawyer_document(
447
  return f"Error retrieving document: {str(e)}"
448
 
449
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
  # Export tool sets for different user types
451
- tools_for_client_facade = [query_knowledge_graph, find_lawyers, message_lawyer, search_web, edit_document]
452
- tools_for_client = [_query_knowledge_graph, _find_lawyers, _message_lawyer, search_web, _edit_document]
453
- tools_for_lawyer_facade = [query_knowledge_graph, search_web, edit_document, retrieve_lawyer_document]
454
- tools_for_lawyer = [_query_knowledge_graph, search_web, _edit_document, _retrieve_lawyer_document]
 
 
 
 
 
455
  tools = tools_for_client
 
17
  # Global instances - will be initialized in agent_api.py
18
  lawyer_selector_agent: Optional[LawyerSelectorAgent] = None
19
  lawyer_messenger_agent: Optional[LawyerMessengerAgent] = None
 
20
  lightrag_client: Optional[LightRAGClient] = None
21
+ tavily_search: Optional[TavilySearch] = None
22
  resend_api_key: Optional[str] = None
23
+ doc_editor_agent: Optional[DocumentEditorAgent] = None
24
 
25
  @tool
26
  async def query_knowledge_graph(
 
249
  except Exception as e:
250
  return f"Error sending message to lawyer: {str(e)}"
251
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  @tool
253
  async def retrieve_lawyer_document(file_path: str) -> str:
254
  """
 
357
  return f"Error retrieving document: {str(e)}"
358
 
359
 
360
+ # ============ DOC ASSISTANT TOOLS ============
361
+
362
+ @tool
363
+ async def edit_document(plan: str) -> str:
364
+ """
365
+ Edit the HTML document by providing a detailed execution plan.
366
+
367
+ This tool is a facade - the router agent calls this when it decides that
368
+ document editing is needed. The actual editing will be performed by the
369
+ DocumentEditorAgent.
370
+
371
+ Args:
372
+ plan: A numbered list of up to 6 steps describing exactly how to execute the modification
373
+
374
+ Returns:
375
+ Placeholder response - actual implementation is in the router
376
+
377
+ Example:
378
+ plan = "1. Find the paragraph containing '12 months'\n2. Replace '12 months' with '24 months'\n3. Verify the change is correct"
379
+ """
380
+ return
381
+
382
+
383
+ @tool
384
+ async def _edit_document(
385
+ doc_text: str,
386
+ user_instruction: str,
387
+ plan: str,
388
+ doc_summaries: list = [],
389
+ conversation_history: list = [],
390
+ max_iterations: int = 10,
391
+ document_id: str = None,
392
+ user_id: str = None
393
+ ) -> str:
394
+ """
395
+ Edit the HTML document by launching the DocumentEditorAgent workflow.
396
+
397
+ This is the real implementation that actually executes the document editor
398
+ graph with all necessary parameters.
399
+
400
+ Args:
401
+ doc_text: The HTML document content
402
+ user_instruction: User's instruction for editing
403
+ plan: A numbered list of up to 6 steps describing exactly how to execute the modification
404
+ doc_summaries: Optional list of document summaries for context
405
+ conversation_history: Optional conversation history
406
+ max_iterations: Maximum number of edit iterations (default: 10)
407
+ document_id: Optional UUID of the document for live updates
408
+ user_id: Optional user ID for authentication
409
+
410
+ Returns:
411
+ Result message from the editor agent
412
+ """
413
+ try:
414
+ if doc_editor_agent is None:
415
+ raise ValueError("DocumentEditorAgent not initialized. Please initialize it in agent_api.py")
416
+
417
+ # Invoke the document editor agent
418
+ result = await doc_editor_agent.edit_document(
419
+ doc_text=doc_text,
420
+ user_instruction=user_instruction,
421
+ plan=plan,
422
+ doc_summaries=doc_summaries,
423
+ conversation_history=conversation_history,
424
+ max_iterations=max_iterations,
425
+ document_id=document_id,
426
+ user_id=user_id
427
+ )
428
+
429
+ # Return the result as a string
430
+ if result.get("success"):
431
+ return f"Document edited successfully: {result.get('message', 'Completed')}"
432
+ else:
433
+ return f"Document editing failed: {result.get('message', 'Unknown error')}"
434
+
435
+ except Exception as e:
436
+ return f"Error editing document: {str(e)}"
437
+
438
+
439
  # Export tool sets for different user types
440
+ tools_for_client_facade = [query_knowledge_graph, find_lawyers, message_lawyer, search_web]
441
+ tools_for_client = [_query_knowledge_graph, _find_lawyers, _message_lawyer, search_web ]
442
+ tools_for_lawyer_facade = [query_knowledge_graph, search_web, retrieve_lawyer_document]
443
+ tools_for_lawyer = [_query_knowledge_graph, search_web, _retrieve_lawyer_document]
444
+
445
+ # Tools for DocAssistant (document router)
446
+ tools_for_doc_facade = [query_knowledge_graph, retrieve_lawyer_document, edit_document]
447
+ tools_for_doc = [_query_knowledge_graph, _retrieve_lawyer_document, _edit_document]
448
+
449
  tools = tools_for_client