Spaces:
Runtime error
Runtime error
Charles Grandjean commited on
Commit Β·
6dd54b4
1
Parent(s): 5a8db1f
add the doc_editor to chat agent
Browse files- agent_api.py +5 -0
- agents/chat_agent.py +8 -0
- prompts/main.py +12 -0
- requirements.txt +1 -1
- tests/test_create_draft_document.py +55 -45
- utils/tools.py +173 -37
agent_api.py
CHANGED
|
@@ -36,6 +36,7 @@ from agents.pdf_analyzer import PDFAnalyzerAgent
|
|
| 36 |
from agents.doc_editor import DocumentEditorAgent
|
| 37 |
from agents.doc_assistant import DocAssistant
|
| 38 |
from langchain_openai import ChatOpenAI
|
|
|
|
| 39 |
from mistralai import Mistral
|
| 40 |
import logging
|
| 41 |
import traceback
|
|
@@ -92,6 +93,10 @@ class LLMConfig:
|
|
| 92 |
# api_key=os.getenv("GOOGLE_API_KEY"),
|
| 93 |
# thinking_level="low"
|
| 94 |
# )
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
# logger.info("β
LLMConfig initialized:")
|
| 96 |
# logger.info(f" - OpenAI LLM: {os.getenv('LLM_MODEL', 'gpt-5-nano-2025-08-07')}")
|
| 97 |
# logger.info(f" - Gemini LLM: {os.getenv('GEMINI_TOOL_MODEL', 'gemini-3-flash-preview')} (for tool calling)")
|
|
|
|
| 36 |
from agents.doc_editor import DocumentEditorAgent
|
| 37 |
from agents.doc_assistant import DocAssistant
|
| 38 |
from langchain_openai import ChatOpenAI
|
| 39 |
+
from langchain_xai import ChatXAI
|
| 40 |
from mistralai import Mistral
|
| 41 |
import logging
|
| 42 |
import traceback
|
|
|
|
| 93 |
# api_key=os.getenv("GOOGLE_API_KEY"),
|
| 94 |
# thinking_level="low"
|
| 95 |
# )
|
| 96 |
+
self.llm = ChatXAI(
|
| 97 |
+
model=os.getenv("XAI_TOOL_MODEL"),
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
# logger.info("β
LLMConfig initialized:")
|
| 101 |
# logger.info(f" - OpenAI LLM: {os.getenv('LLM_MODEL', 'gpt-5-nano-2025-08-07')}")
|
| 102 |
# logger.info(f" - Gemini LLM: {os.getenv('GEMINI_TOOL_MODEL', 'gemini-3-flash-preview')} (for tool calling)")
|
agents/chat_agent.py
CHANGED
|
@@ -123,6 +123,14 @@ class CyberLegalAgent:
|
|
| 123 |
# Inject user_id for create_draft_document tool
|
| 124 |
if tool_call['name'] == "create_draft_document":
|
| 125 |
args["user_id"] = state.get("user_id")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
logger.info(f"π Injecting user_id for create_draft_document: {args['user_id']}")
|
| 127 |
|
| 128 |
# Convert to implementation name
|
|
|
|
| 123 |
# Inject user_id for create_draft_document tool
|
| 124 |
if tool_call['name'] == "create_draft_document":
|
| 125 |
args["user_id"] = state.get("user_id")
|
| 126 |
+
args["instruction"] = state.get("user_query", "")
|
| 127 |
+
# Inject conversation_history for context
|
| 128 |
+
args["conversation_history"] = state.get("conversation_history", [])
|
| 129 |
+
logger.info(f"π¬ Injecting conversation_history for create_draft_document: {len(args['conversation_history'])} messages")
|
| 130 |
+
# Inject doc_summaries for context
|
| 131 |
+
args["doc_summaries"] = state.get("doc_summaries", [])
|
| 132 |
+
logger.info(f"π Injecting doc_summaries for create_draft_document: {len(args['doc_summaries'])} documents")
|
| 133 |
+
logger.info(f"π Using user_query as instruction for create_draft_document")
|
| 134 |
logger.info(f"π Injecting user_id for create_draft_document: {args['user_id']}")
|
| 135 |
|
| 136 |
# Convert to implementation name
|
prompts/main.py
CHANGED
|
@@ -75,6 +75,12 @@ You are one of the agent in Hexiagon AI platform, a juridical platform that help
|
|
| 75 |
1. **query_knowledge_graph**: Search legal documents to answer questions about law, regulations and directives.
|
| 76 |
2. **search_web**: Search the web for current information, court decisions, or news that may not be in the knowledge graph.
|
| 77 |
3. **retrieve_lawyer_document**: Retrieve the full content of a specific document from your document database. Use this tool when you need to inspect the detailed text, clauses, or specific provisions of a document mentioned in your documents_tree. But avoid it if the summary key details and actors are sufficient as it can introduce long document in the chat.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
### Tool-Calling Process
|
| 80 |
You operate in an iterative loop:
|
|
@@ -89,6 +95,12 @@ You operate in an iterative loop:
|
|
| 89 |
3. Analyze legal implications, potential interpretations, and relevant jurisprudence where applicable.
|
| 90 |
4. Create a section at the end of your response called "References" that lists the source documents used to answer the user's question with full legal citations.
|
| 91 |
5. If the user asks a specific detail about a document from your documents_tree, use the retrieve_lawyer_document tool, but avoid it if the summary key details and actors are sufficient as it can introduce long document in the chat.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
### Tone
|
| 94 |
- Professional and authoritative
|
|
|
|
| 75 |
1. **query_knowledge_graph**: Search legal documents to answer questions about law, regulations and directives.
|
| 76 |
2. **search_web**: Search the web for current information, court decisions, or news that may not be in the knowledge graph.
|
| 77 |
3. **retrieve_lawyer_document**: Retrieve the full content of a specific document from your document database. Use this tool when you need to inspect the detailed text, clauses, or specific provisions of a document mentioned in your documents_tree. But avoid it if the summary key details and actors are sufficient as it can introduce long document in the chat.
|
| 78 |
+
4. **create_draft_document**: Create a new document draft by building it from scratch using an execution plan. This tool creates an empty document, then uses the doc_editor agent to build it step-by-step according to your plan, and saves it directly to your "My documents" section on the platform. The document is created with full access to the conversation history and relevant document summaries for context. Use this when the user asks you to create, draft, or build a new legal document (contract, agreement, letter, etc.). You will need to provide:
|
| 79 |
+
- `path_with_filename`: Full path and filename with extension (e.g., "Contracts/Contrat_bail.pdf" or "Courriers/Lettre_relance.pdf")
|
| 80 |
+
- `plan_to_build_document`: A numbered list of 1-6 steps describing how to build the document (e.g., "1. Create header with title\n2. Add section for parties\n3. Add main clauses\n4. Add signature section")
|
| 81 |
+
- `instruction`: The user's instruction for what to create (this is automatically provided from their query)
|
| 82 |
+
|
| 83 |
+
**CRITICAL**: Before calling create_draft_document, you MUST ensure the request is sufficiently clear and detailed. If the user's request is vague or missing essential information (e.g., "create a contract" without specifying type, parties, key terms), ask clarifying questions first. Only proceed with document creation when you have enough details to create a meaningful, complete document.
|
| 84 |
|
| 85 |
### Tool-Calling Process
|
| 86 |
You operate in an iterative loop:
|
|
|
|
| 95 |
3. Analyze legal implications, potential interpretations, and relevant jurisprudence where applicable.
|
| 96 |
4. Create a section at the end of your response called "References" that lists the source documents used to answer the user's question with full legal citations.
|
| 97 |
5. If the user asks a specific detail about a document from your documents_tree, use the retrieve_lawyer_document tool, but avoid it if the summary key details and actors are sufficient as it can introduce long document in the chat.
|
| 98 |
+
6. **CRITICAL - Before creating documents**: When the user asks you to create a document, you MUST ensure the request is sufficiently clear and detailed. If the user's request is vague or missing essential information (e.g., "create a contract" without specifying type, parties, key terms), ask clarifying questions first. Only proceed with document creation when you have enough details to create a meaningful, complete document.
|
| 99 |
+
7. **After creating documents**: When you create a document using the create_draft_document tool, you MUST clearly explain to the user:
|
| 100 |
+
- What document was created (title, type)
|
| 101 |
+
- What the document contains (summary of main sections)
|
| 102 |
+
- **EXACTLY where the document was saved** in their "My documents" section (full path with filename, e.g., "The document has been saved to: Contracts/Contrat_bail.pdf in your My documents section")
|
| 103 |
+
- This information is critical so the user can find and access their document
|
| 104 |
|
| 105 |
### Tone
|
| 106 |
- Professional and authoritative
|
requirements.txt
CHANGED
|
@@ -13,7 +13,7 @@ langchain-openai>=0.1.0
|
|
| 13 |
langchain-community>=0.0.20
|
| 14 |
langchain-google-genai>=1.0.0
|
| 15 |
mistralai>=1.0.0
|
| 16 |
-
|
| 17 |
# FastAPI and server dependencies
|
| 18 |
fastapi>=0.104.0
|
| 19 |
uvicorn[standard]>=0.24.0
|
|
|
|
| 13 |
langchain-community>=0.0.20
|
| 14 |
langchain-google-genai>=1.0.0
|
| 15 |
mistralai>=1.0.0
|
| 16 |
+
langchain-xai==1.2.2
|
| 17 |
# FastAPI and server dependencies
|
| 18 |
fastapi>=0.104.0
|
| 19 |
uvicorn[standard]>=0.24.0
|
tests/test_create_draft_document.py
CHANGED
|
@@ -20,9 +20,9 @@ async def test_tool_facade():
|
|
| 20 |
|
| 21 |
# This should return immediately (facade just returns)
|
| 22 |
result = await create_draft_document.ainvoke({
|
| 23 |
-
"
|
| 24 |
-
"
|
| 25 |
-
"
|
| 26 |
})
|
| 27 |
|
| 28 |
print(f"β
Facade test passed: {result}")
|
|
@@ -36,38 +36,46 @@ async def test_tool_real():
|
|
| 36 |
# Mock user_id
|
| 37 |
test_user_id = os.getenv("TEST_USER_ID", "test-user-uuid-123")
|
| 38 |
|
| 39 |
-
# Test with various paths
|
| 40 |
test_cases = [
|
| 41 |
{
|
| 42 |
-
"
|
| 43 |
-
"
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
| 46 |
},
|
| 47 |
{
|
| 48 |
-
"
|
| 49 |
-
"
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
| 52 |
},
|
| 53 |
{
|
| 54 |
-
"
|
| 55 |
-
"
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
| 58 |
}
|
| 59 |
]
|
| 60 |
|
| 61 |
for i, test_case in enumerate(test_cases, 1):
|
| 62 |
-
print(f"\n Test case {i}: {test_case['
|
| 63 |
-
print(f"
|
| 64 |
print(f" Expected: {test_case['expected_path']}")
|
| 65 |
|
| 66 |
result = await _create_draft_document.ainvoke({
|
| 67 |
"user_id": test_user_id,
|
| 68 |
-
"
|
| 69 |
-
"
|
| 70 |
-
"
|
| 71 |
})
|
| 72 |
|
| 73 |
print(f" Result: {result}")
|
|
@@ -82,39 +90,41 @@ async def test_tool_real():
|
|
| 82 |
return True
|
| 83 |
|
| 84 |
|
| 85 |
-
async def
|
| 86 |
-
"""Test path
|
| 87 |
-
print("\nπ§ͺ Testing path
|
| 88 |
|
| 89 |
test_cases = [
|
| 90 |
-
("Contracts/", "
|
| 91 |
-
("
|
| 92 |
-
("", "
|
| 93 |
-
("./Contracts/", "./Contracts
|
| 94 |
-
("Drafts/Legal/", "./Drafts/Legal/"),
|
| 95 |
-
("./Drafts/Legal", "./Drafts/Legal/")
|
| 96 |
]
|
| 97 |
|
| 98 |
-
for
|
| 99 |
-
# Simulate the path
|
| 100 |
-
|
| 101 |
-
if
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
if not path.endswith('/'):
|
| 105 |
-
path += '/'
|
| 106 |
else:
|
| 107 |
path = ''
|
|
|
|
| 108 |
|
| 109 |
-
|
|
|
|
| 110 |
|
| 111 |
-
print(f" Input: '{
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
print(f" β
Correct")
|
| 114 |
else:
|
| 115 |
-
print(f" β οΈ
|
| 116 |
|
| 117 |
-
print("\nβ
Path
|
| 118 |
return True
|
| 119 |
|
| 120 |
|
|
@@ -131,8 +141,8 @@ async def main():
|
|
| 131 |
# Test real implementation
|
| 132 |
await test_tool_real()
|
| 133 |
|
| 134 |
-
# Test path
|
| 135 |
-
await
|
| 136 |
|
| 137 |
print("\n" + "=" * 80)
|
| 138 |
print("β
All tests completed successfully!")
|
|
|
|
| 20 |
|
| 21 |
# This should return immediately (facade just returns)
|
| 22 |
result = await create_draft_document.ainvoke({
|
| 23 |
+
"path_with_filename": "Test/Test_Document.pdf",
|
| 24 |
+
"plan_to_build_document": "1. Create title\n2. Add content",
|
| 25 |
+
"instruction": "Create a test document"
|
| 26 |
})
|
| 27 |
|
| 28 |
print(f"β
Facade test passed: {result}")
|
|
|
|
| 36 |
# Mock user_id
|
| 37 |
test_user_id = os.getenv("TEST_USER_ID", "test-user-uuid-123")
|
| 38 |
|
| 39 |
+
# Test with various paths and plans
|
| 40 |
test_cases = [
|
| 41 |
{
|
| 42 |
+
"path_with_filename": "Contracts/Contract_de_bail.pdf",
|
| 43 |
+
"plan_to_build_document": """1. Create header with title "Contrat de bail"
|
| 44 |
+
2. Add section for parties (bailleur and preneur)
|
| 45 |
+
3. Add main clauses for the lease
|
| 46 |
+
4. Add signature section""",
|
| 47 |
+
"instruction": "Create a commercial lease contract",
|
| 48 |
+
"expected_path": "Contracts/Contract_de_bail.pdf"
|
| 49 |
},
|
| 50 |
{
|
| 51 |
+
"path_with_filename": "Mon_document.pdf",
|
| 52 |
+
"plan_to_build_document": """1. Add document title
|
| 53 |
+
2. Add introductory paragraph
|
| 54 |
+
3. Add main content sections""",
|
| 55 |
+
"instruction": "Create a simple document",
|
| 56 |
+
"expected_path": "Mon_document.pdf"
|
| 57 |
},
|
| 58 |
{
|
| 59 |
+
"path_with_filename": "Drafts/Legal/Note_juridique.pdf",
|
| 60 |
+
"plan_to_build_document": """1. Create title "Note juridique"
|
| 61 |
+
2. Add legal context
|
| 62 |
+
3. Add analysis section
|
| 63 |
+
4. Add conclusions""",
|
| 64 |
+
"instruction": "Create a legal note",
|
| 65 |
+
"expected_path": "Drafts/Legal/Note_juridique.pdf"
|
| 66 |
}
|
| 67 |
]
|
| 68 |
|
| 69 |
for i, test_case in enumerate(test_cases, 1):
|
| 70 |
+
print(f"\n Test case {i}: {test_case['path_with_filename']}")
|
| 71 |
+
print(f" Instruction: {test_case['instruction']}")
|
| 72 |
print(f" Expected: {test_case['expected_path']}")
|
| 73 |
|
| 74 |
result = await _create_draft_document.ainvoke({
|
| 75 |
"user_id": test_user_id,
|
| 76 |
+
"path_with_filename": test_case["path_with_filename"],
|
| 77 |
+
"plan_to_build_document": test_case["plan_to_build_document"],
|
| 78 |
+
"instruction": test_case["instruction"]
|
| 79 |
})
|
| 80 |
|
| 81 |
print(f" Result: {result}")
|
|
|
|
| 90 |
return True
|
| 91 |
|
| 92 |
|
| 93 |
+
async def test_path_parsing():
|
| 94 |
+
"""Test path parsing logic"""
|
| 95 |
+
print("\nπ§ͺ Testing path parsing...")
|
| 96 |
|
| 97 |
test_cases = [
|
| 98 |
+
("Contracts/Test.pdf", "Contracts", "Test.pdf", "Test"),
|
| 99 |
+
("Test.pdf", "", "Test.pdf", "Test"),
|
| 100 |
+
("Drafts/Legal/Note.pdf", "Drafts/Legal", "Note.pdf", "Note"),
|
| 101 |
+
("./Contracts/Test.pdf", "./Contracts", "Test.pdf", "Test"),
|
|
|
|
|
|
|
| 102 |
]
|
| 103 |
|
| 104 |
+
for path_with_filename, expected_path, expected_filename, expected_title in test_cases:
|
| 105 |
+
# Simulate the path parsing logic
|
| 106 |
+
parts = path_with_filename.split('/')
|
| 107 |
+
if len(parts) > 1:
|
| 108 |
+
path = '/'.join(parts[:-1])
|
| 109 |
+
filename = parts[-1]
|
|
|
|
|
|
|
| 110 |
else:
|
| 111 |
path = ''
|
| 112 |
+
filename = parts[0]
|
| 113 |
|
| 114 |
+
# Remove .pdf extension
|
| 115 |
+
title = filename.replace('.pdf', '')
|
| 116 |
|
| 117 |
+
print(f" Input: '{path_with_filename}'")
|
| 118 |
+
print(f" β Path: '{path}'")
|
| 119 |
+
print(f" β Filename: '{filename}'")
|
| 120 |
+
print(f" β Title: '{title}'")
|
| 121 |
+
|
| 122 |
+
if path == expected_path and filename == expected_filename and title == expected_title:
|
| 123 |
print(f" β
Correct")
|
| 124 |
else:
|
| 125 |
+
print(f" β οΈ Mismatch - Expected: path='{expected_path}', filename='{expected_filename}', title='{expected_title}'")
|
| 126 |
|
| 127 |
+
print("\nβ
Path parsing test completed")
|
| 128 |
return True
|
| 129 |
|
| 130 |
|
|
|
|
| 141 |
# Test real implementation
|
| 142 |
await test_tool_real()
|
| 143 |
|
| 144 |
+
# Test path parsing
|
| 145 |
+
await test_path_parsing()
|
| 146 |
|
| 147 |
print("\n" + "=" * 80)
|
| 148 |
print("β
All tests completed successfully!")
|
utils/tools.py
CHANGED
|
@@ -363,59 +363,192 @@ async def _retrieve_lawyer_document(
|
|
| 363 |
|
| 364 |
@tool
|
| 365 |
async def create_draft_document(
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
) -> str:
|
| 370 |
"""
|
| 371 |
-
Create a new document draft
|
| 372 |
|
| 373 |
-
This tool
|
| 374 |
-
|
| 375 |
|
| 376 |
-
Use this tool when
|
| 377 |
-
- Create a new document
|
| 378 |
-
-
|
| 379 |
-
-
|
| 380 |
|
| 381 |
Args:
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
- Empty string "" β root folder of My Documents
|
| 386 |
-
- "Contracts/" β ./Contracts/title.pdf
|
| 387 |
-
- "Drafts/Legal/" β ./Drafts/Legal/title.pdf
|
| 388 |
|
| 389 |
Returns:
|
| 390 |
-
Confirmation message with document path and
|
| 391 |
|
| 392 |
Example:
|
| 393 |
create_draft_document(
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
)
|
| 398 |
-
β
|
| 399 |
"""
|
| 400 |
return
|
| 401 |
|
| 402 |
@tool
|
| 403 |
async def _create_draft_document(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
user_id: str,
|
| 405 |
title: str,
|
| 406 |
content: str,
|
| 407 |
path: str
|
| 408 |
-
) -> str:
|
| 409 |
"""
|
| 410 |
-
|
|
|
|
| 411 |
Args:
|
| 412 |
-
user_id: User UUID
|
| 413 |
title: Document title
|
| 414 |
content: Document HTML content
|
| 415 |
path: Folder path
|
| 416 |
|
| 417 |
Returns:
|
| 418 |
-
Success/failure message
|
| 419 |
"""
|
| 420 |
try:
|
| 421 |
import httpx
|
|
@@ -436,6 +569,7 @@ async def _create_draft_document(
|
|
| 436 |
|
| 437 |
full_path = f"./{path}{title}.pdf"
|
| 438 |
|
|
|
|
| 439 |
endpoint_url = f"{base_url}/functions/v1/create-document-from-html"
|
| 440 |
|
| 441 |
request_body = {
|
|
@@ -455,25 +589,27 @@ async def _create_draft_document(
|
|
| 455 |
)
|
| 456 |
|
| 457 |
if response.status_code == 200:
|
| 458 |
-
|
|
|
|
|
|
|
| 459 |
elif response.status_code == 400:
|
| 460 |
error_data = response.json() if response.headers.get('content-type', '').startswith('application/json') else {}
|
| 461 |
-
return f"β Bad request: {error_data.get('error', 'Unknown error')}"
|
| 462 |
elif response.status_code == 401:
|
| 463 |
-
return "β Authentication failed: Invalid API key"
|
| 464 |
elif response.status_code == 403:
|
| 465 |
-
return "β Access denied: You do not have permission to save documents"
|
| 466 |
elif response.status_code == 500:
|
| 467 |
-
return "β Server error: Failed to save document"
|
| 468 |
else:
|
| 469 |
-
return f"β Error: HTTP {response.status_code} - {response.text}"
|
| 470 |
|
| 471 |
except httpx.TimeoutError:
|
| 472 |
-
return "β Error: Timeout while saving document"
|
| 473 |
except httpx.RequestError as e:
|
| 474 |
-
return f"β Error: Failed to connect to document server: {str(e)}"
|
| 475 |
except Exception as e:
|
| 476 |
-
return f"β Error saving document: {str(e)}"
|
| 477 |
|
| 478 |
|
| 479 |
# ============ DOC ASSISTANT TOOLS ============
|
|
@@ -941,8 +1077,8 @@ async def _attempt_completion(message: str) -> Dict[str, Any]:
|
|
| 941 |
|
| 942 |
|
| 943 |
# Export tool sets for different user types
|
| 944 |
-
tools_for_client_facade = [query_knowledge_graph, find_lawyers, message_lawyer, search_web
|
| 945 |
-
tools_for_client = [_query_knowledge_graph, _find_lawyers, _message_lawyer, search_web
|
| 946 |
tools_for_lawyer_facade = [query_knowledge_graph, search_web, retrieve_lawyer_document, create_draft_document]
|
| 947 |
tools_for_lawyer = [_query_knowledge_graph, search_web, _retrieve_lawyer_document, _create_draft_document]
|
| 948 |
|
|
@@ -953,4 +1089,4 @@ tools_for_doc_assistant = [_query_knowledge_graph, _retrieve_lawyer_document, _e
|
|
| 953 |
tools_for_doc_editor_facade = [replace_html, add_html, delete_html, view_current_document, attempt_completion]
|
| 954 |
tools_for_doc_editor = [_replace_html, _add_html, _delete_html, _view_current_document, _attempt_completion]
|
| 955 |
|
| 956 |
-
tools = tools_for_client
|
|
|
|
| 363 |
|
| 364 |
@tool
|
| 365 |
async def create_draft_document(
|
| 366 |
+
path_with_filename: str,
|
| 367 |
+
plan_to_build_document: str,
|
| 368 |
+
instruction: str
|
| 369 |
) -> str:
|
| 370 |
"""
|
| 371 |
+
Create a new document draft by building it with doc_editor and save to "My documents" via Supabase.
|
| 372 |
|
| 373 |
+
This tool creates an empty document, then uses doc_editor to build it according to a plan,
|
| 374 |
+
and finally saves the complete document to the user's document storage.
|
| 375 |
|
| 376 |
+
Use this tool when you want to:
|
| 377 |
+
- Create a new document from scratch
|
| 378 |
+
- Build a document using an execution plan
|
| 379 |
+
- Leverage the doc_editor's iterative editing capabilities
|
| 380 |
|
| 381 |
Args:
|
| 382 |
+
path_with_filename: Full path and filename with extension (e.g., "Contracts/Contrat_bail.pdf")
|
| 383 |
+
plan_to_build_document: A numbered list of steps to build the document (1-6 steps recommended)
|
| 384 |
+
instruction: The user's instruction for what to create (e.g., "Create a commercial lease contract")
|
|
|
|
|
|
|
|
|
|
| 385 |
|
| 386 |
Returns:
|
| 387 |
+
Confirmation message with document path and summary of what was created
|
| 388 |
|
| 389 |
Example:
|
| 390 |
create_draft_document(
|
| 391 |
+
path_with_filename="Contracts/Contrat_bail.pdf",
|
| 392 |
+
plan_to_build_document="1. Create section for parties\\n2. Add main clauses\\n3. Add signature section",
|
| 393 |
+
instruction="Create a commercial lease contract"
|
| 394 |
)
|
| 395 |
+
β Creates document via doc_editor and saves as "./Contracts/Contrat_bail.pdf"
|
| 396 |
"""
|
| 397 |
return
|
| 398 |
|
| 399 |
@tool
|
| 400 |
async def _create_draft_document(
|
| 401 |
+
user_id: str,
|
| 402 |
+
path_with_filename: str,
|
| 403 |
+
plan_to_build_document: str,
|
| 404 |
+
instruction: str,
|
| 405 |
+
doc_summaries: list = [],
|
| 406 |
+
conversation_history: list = []
|
| 407 |
+
) -> str:
|
| 408 |
+
"""
|
| 409 |
+
Create a document by building it with doc_editor and save to Supabase.
|
| 410 |
+
|
| 411 |
+
Args:
|
| 412 |
+
user_id: User UUID (injected by the agent)
|
| 413 |
+
path_with_filename: Full path and filename (e.g., "Contracts/Contrat_bail.pdf")
|
| 414 |
+
plan_to_build_document: Execution plan for building the document
|
| 415 |
+
instruction: User's instruction for what to create
|
| 416 |
+
doc_summaries: Optional list of document summaries for context (injected from state)
|
| 417 |
+
conversation_history: Optional conversation history for context (injected from state)
|
| 418 |
+
|
| 419 |
+
Returns:
|
| 420 |
+
Success/failure message with document path and summary
|
| 421 |
+
"""
|
| 422 |
+
try:
|
| 423 |
+
# Parse path and filename
|
| 424 |
+
parts = path_with_filename.split('/')
|
| 425 |
+
if len(parts) > 1:
|
| 426 |
+
path = '/'.join(parts[:-1])
|
| 427 |
+
filename = parts[-1]
|
| 428 |
+
else:
|
| 429 |
+
path = ''
|
| 430 |
+
filename = parts[0]
|
| 431 |
+
|
| 432 |
+
# Remove .pdf extension if present
|
| 433 |
+
title = filename.replace('.pdf', '')
|
| 434 |
+
|
| 435 |
+
# 1. Create empty HTML document
|
| 436 |
+
empty_html = """<!DOCTYPE html>
|
| 437 |
+
<html>
|
| 438 |
+
<head>
|
| 439 |
+
<meta charset="UTF-8">
|
| 440 |
+
<title>Document</title>
|
| 441 |
+
</head>
|
| 442 |
+
<body>
|
| 443 |
+
</body>
|
| 444 |
+
</html>"""
|
| 445 |
+
|
| 446 |
+
# 2. Save empty document and get document_id
|
| 447 |
+
logger.info("=" * 80)
|
| 448 |
+
logger.info("π STEP 1: Creating empty document on Supabase")
|
| 449 |
+
logger.info("=" * 80)
|
| 450 |
+
logger.info(f" User ID: {user_id}")
|
| 451 |
+
logger.info(f" Title: {title}")
|
| 452 |
+
logger.info(f" Path: {path if path else 'root'}")
|
| 453 |
+
logger.info(f" Empty HTML size: {len(empty_html)} bytes")
|
| 454 |
+
|
| 455 |
+
msg, document_id = await _create_draft_document_impl(
|
| 456 |
+
user_id=user_id,
|
| 457 |
+
title=title,
|
| 458 |
+
content=empty_html,
|
| 459 |
+
path=path
|
| 460 |
+
)
|
| 461 |
+
|
| 462 |
+
if not document_id:
|
| 463 |
+
logger.error(f"β Failed to create empty document: {msg}")
|
| 464 |
+
return f"β Failed to create document: {msg}"
|
| 465 |
+
|
| 466 |
+
logger.info(f"β
Empty document created successfully!")
|
| 467 |
+
logger.info(f" Document ID: {document_id}")
|
| 468 |
+
logger.info(f" Full path: {msg}")
|
| 469 |
+
logger.info("")
|
| 470 |
+
|
| 471 |
+
# 3. Launch doc_editor to build the document with real-time updates
|
| 472 |
+
logger.info("=" * 80)
|
| 473 |
+
logger.info("π§ STEP 2: Launching doc_editor to build document")
|
| 474 |
+
logger.info("=" * 80)
|
| 475 |
+
logger.info(f" Document title: {title}")
|
| 476 |
+
logger.info(f" Document ID: {document_id}")
|
| 477 |
+
logger.info(f" User ID: {user_id}")
|
| 478 |
+
logger.info(f" Instruction: {instruction}")
|
| 479 |
+
logger.info(f" Plan length: {len(plan_to_build_document)} characters")
|
| 480 |
+
logger.info(f" Max iterations: 10")
|
| 481 |
+
logger.info(f" Real-time updates: ENABLED (will push to Supabase after each edit)")
|
| 482 |
+
logger.info("")
|
| 483 |
+
|
| 484 |
+
editor_result = await doc_editor_agent.edit_document(
|
| 485 |
+
doc_text=empty_html,
|
| 486 |
+
user_instruction=instruction,
|
| 487 |
+
plan=plan_to_build_document,
|
| 488 |
+
doc_summaries=doc_summaries,
|
| 489 |
+
conversation_history=conversation_history,
|
| 490 |
+
max_iterations=10,
|
| 491 |
+
document_id=document_id, # Pass document_id for real-time updates
|
| 492 |
+
user_id=user_id # Pass user_id for authentication
|
| 493 |
+
)
|
| 494 |
+
|
| 495 |
+
# 4. Return result (no need to save again - doc_editor handled real-time updates)
|
| 496 |
+
if editor_result.get('success'):
|
| 497 |
+
summary = editor_result.get('final_summary', 'No summary available')
|
| 498 |
+
iteration_count = editor_result.get('iteration_count', 0)
|
| 499 |
+
final_doc_size = len(editor_result.get('doc_text', ''))
|
| 500 |
+
|
| 501 |
+
logger.info("=" * 80)
|
| 502 |
+
logger.info("β
STEP 3: Document creation completed successfully!")
|
| 503 |
+
logger.info("=" * 80)
|
| 504 |
+
logger.info(f" Document path: {path_with_filename}")
|
| 505 |
+
logger.info(f" Document ID: {document_id}")
|
| 506 |
+
logger.info(f" Iterations completed: {iteration_count}")
|
| 507 |
+
logger.info(f" Final document size: {final_doc_size} bytes")
|
| 508 |
+
logger.info(f" Summary: {summary}")
|
| 509 |
+
logger.info("")
|
| 510 |
+
logger.info("π Document created via real-time updates:")
|
| 511 |
+
logger.info(" - Empty document created on Supabase")
|
| 512 |
+
logger.info(" - doc_editor built document iteratively")
|
| 513 |
+
logger.info(" - Each edit pushed to Supabase in real-time")
|
| 514 |
+
logger.info(" - No final save needed (already up-to-date)")
|
| 515 |
+
logger.info("=" * 80)
|
| 516 |
+
|
| 517 |
+
return f"β
Document created: {path_with_filename}\n\n{summary}"
|
| 518 |
+
else:
|
| 519 |
+
logger.error("=" * 80)
|
| 520 |
+
logger.error("β STEP 3: Document creation failed!")
|
| 521 |
+
logger.error("=" * 80)
|
| 522 |
+
logger.error(f" Document path: {path_with_filename}")
|
| 523 |
+
logger.error(f" Document ID: {document_id}")
|
| 524 |
+
logger.error(f" Error message: {editor_result.get('message', 'Unknown error')}")
|
| 525 |
+
logger.error(f" Success flag: {editor_result.get('success')}")
|
| 526 |
+
logger.error("=" * 80)
|
| 527 |
+
|
| 528 |
+
return f"β Document creation failed: {editor_result.get('message', 'Unknown error')}"
|
| 529 |
+
|
| 530 |
+
except Exception as e:
|
| 531 |
+
logger.error(f"Error in _create_draft_document: {str(e)}")
|
| 532 |
+
return f"β Error creating document: {str(e)}"
|
| 533 |
+
|
| 534 |
+
|
| 535 |
+
async def _create_draft_document_impl(
|
| 536 |
user_id: str,
|
| 537 |
title: str,
|
| 538 |
content: str,
|
| 539 |
path: str
|
| 540 |
+
) -> tuple[str, str]:
|
| 541 |
"""
|
| 542 |
+
Helper function to save a document to Supabase (implementation).
|
| 543 |
+
|
| 544 |
Args:
|
| 545 |
+
user_id: User UUID
|
| 546 |
title: Document title
|
| 547 |
content: Document HTML content
|
| 548 |
path: Folder path
|
| 549 |
|
| 550 |
Returns:
|
| 551 |
+
tuple: (message, document_id) - Success/failure message and document_id (or None on error)
|
| 552 |
"""
|
| 553 |
try:
|
| 554 |
import httpx
|
|
|
|
| 569 |
|
| 570 |
full_path = f"./{path}{title}.pdf"
|
| 571 |
|
| 572 |
+
# URL already has /functions/v1/ (correct)
|
| 573 |
endpoint_url = f"{base_url}/functions/v1/create-document-from-html"
|
| 574 |
|
| 575 |
request_body = {
|
|
|
|
| 589 |
)
|
| 590 |
|
| 591 |
if response.status_code == 200:
|
| 592 |
+
data = response.json()
|
| 593 |
+
document_id = data.get('document_id')
|
| 594 |
+
return f"β
Document successfully saved to: {full_path}", document_id
|
| 595 |
elif response.status_code == 400:
|
| 596 |
error_data = response.json() if response.headers.get('content-type', '').startswith('application/json') else {}
|
| 597 |
+
return f"β Bad request: {error_data.get('error', 'Unknown error')}", None
|
| 598 |
elif response.status_code == 401:
|
| 599 |
+
return "β Authentication failed: Invalid API key", None
|
| 600 |
elif response.status_code == 403:
|
| 601 |
+
return "β Access denied: You do not have permission to save documents", None
|
| 602 |
elif response.status_code == 500:
|
| 603 |
+
return "β Server error: Failed to save document", None
|
| 604 |
else:
|
| 605 |
+
return f"β Error: HTTP {response.status_code} - {response.text}", None
|
| 606 |
|
| 607 |
except httpx.TimeoutError:
|
| 608 |
+
return "β Error: Timeout while saving document", None
|
| 609 |
except httpx.RequestError as e:
|
| 610 |
+
return f"β Error: Failed to connect to document server: {str(e)}", None
|
| 611 |
except Exception as e:
|
| 612 |
+
return f"β Error saving document: {str(e)}", None
|
| 613 |
|
| 614 |
|
| 615 |
# ============ DOC ASSISTANT TOOLS ============
|
|
|
|
| 1077 |
|
| 1078 |
|
| 1079 |
# Export tool sets for different user types
|
| 1080 |
+
tools_for_client_facade = [query_knowledge_graph, find_lawyers, message_lawyer, search_web]
|
| 1081 |
+
tools_for_client = [_query_knowledge_graph, _find_lawyers, _message_lawyer, search_web]
|
| 1082 |
tools_for_lawyer_facade = [query_knowledge_graph, search_web, retrieve_lawyer_document, create_draft_document]
|
| 1083 |
tools_for_lawyer = [_query_knowledge_graph, search_web, _retrieve_lawyer_document, _create_draft_document]
|
| 1084 |
|
|
|
|
| 1089 |
tools_for_doc_editor_facade = [replace_html, add_html, delete_html, view_current_document, attempt_completion]
|
| 1090 |
tools_for_doc_editor = [_replace_html, _add_html, _delete_html, _view_current_document, _attempt_completion]
|
| 1091 |
|
| 1092 |
+
tools = tools_for_client
|