Spaces:
Running
Running
Charles Grandjean commited on
Commit ·
a8cf49b
1
Parent(s): da5a314
meta agent for doc
Browse files- agent_api.py +18 -13
- agent_states/doc_assistant_state.py +37 -0
- agent_states/doc_editor_state.py +3 -0
- prompts/doc_assistant.py +196 -0
- subagents/doc_assistant.py +355 -0
- subagents/doc_editor.py +8 -3
- utils/conversation_manager.py +0 -129
- utils/lightrag_client.py +0 -44
- utils/tools.py +90 -96
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 509 |
logger.info("=" * 80)
|
| 510 |
-
logger.info("🤖 CALLING
|
| 511 |
logger.info("=" * 80)
|
| 512 |
-
result = await self.
|
| 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("📊
|
| 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 -
|
| 535 |
-
modified_document = result
|
| 536 |
-
if
|
| 537 |
-
logger.info(f"📏 Modified document size: {len(
|
| 538 |
-
logger.info(f"📈 Size change: {len(
|
| 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
|
| 452 |
-
tools_for_client = [_query_knowledge_graph, _find_lawyers, _message_lawyer, search_web
|
| 453 |
-
tools_for_lawyer_facade = [query_knowledge_graph, search_web,
|
| 454 |
-
tools_for_lawyer = [_query_knowledge_graph, search_web,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|