Charles Grandjean commited on
Commit
6dd54b4
Β·
1 Parent(s): 5a8db1f

add the doc_editor to chat agent

Browse files
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
- "title": "Test Document",
24
- "content": "<h1>Test</h1><p>This is a test document</p>",
25
- "path": "Test/"
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
- "title": "Contract de bail",
43
- "content": "<h1>Contrat de bail</h1><p>Ce contrat est conclu entre...</p>",
44
- "path": "Contracts/",
45
- "expected_path": "./Contracts/Contract de bail.pdf"
 
 
 
46
  },
47
  {
48
- "title": "Mon document",
49
- "content": "<h1>Document</h1><p>Contenu...</p>",
50
- "path": "",
51
- "expected_path": "./Mon document.pdf"
 
 
52
  },
53
  {
54
- "title": "Note juridique",
55
- "content": "<h1>Note</h1><p>Contenu juridique...</p>",
56
- "path": "Drafts/Legal/",
57
- "expected_path": "./Drafts/Legal/Note juridique.pdf"
 
 
 
58
  }
59
  ]
60
 
61
  for i, test_case in enumerate(test_cases, 1):
62
- print(f"\n Test case {i}: {test_case['title']}")
63
- print(f" Path: {test_case['path']}")
64
  print(f" Expected: {test_case['expected_path']}")
65
 
66
  result = await _create_draft_document.ainvoke({
67
  "user_id": test_user_id,
68
- "title": test_case["title"],
69
- "content": test_case["content"],
70
- "path": test_case["path"]
71
  })
72
 
73
  print(f" Result: {result}")
@@ -82,39 +90,41 @@ async def test_tool_real():
82
  return True
83
 
84
 
85
- async def test_path_normalization():
86
- """Test path normalization logic"""
87
- print("\nπŸ§ͺ Testing path normalization...")
88
 
89
  test_cases = [
90
- ("Contracts/", "./Contracts/"),
91
- ("Contracts", "./Contracts/"),
92
- ("", "./"),
93
- ("./Contracts/", "./Contracts/"),
94
- ("Drafts/Legal/", "./Drafts/Legal/"),
95
- ("./Drafts/Legal", "./Drafts/Legal/")
96
  ]
97
 
98
- for input_path, expected_suffix in test_cases:
99
- # Simulate the path normalization logic
100
- path = input_path
101
- if path:
102
- if path.startswith('./'):
103
- path = path[2:]
104
- if not path.endswith('/'):
105
- path += '/'
106
  else:
107
  path = ''
 
108
 
109
- full_path = f"./{path}Test.pdf"
 
110
 
111
- print(f" Input: '{input_path}' β†’ Output: '{full_path}'")
112
- if full_path.startswith(expected_suffix):
 
 
 
 
113
  print(f" βœ… Correct")
114
  else:
115
- print(f" ⚠️ Expected to start with: {expected_suffix}")
116
 
117
- print("\nβœ… Path normalization test completed")
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 normalization
135
- await test_path_normalization()
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
- title: str,
367
- content: str,
368
- path: str
369
  ) -> str:
370
  """
371
- Create a new document draft and save it to "My documents" via Supabase.
372
 
373
- This tool saves an HTML document as a PDF draft in the user's document storage.
374
- The document will be stored in the specified folder path within "My Documents".
375
 
376
- Use this tool when the user wants to:
377
- - Create a new document draft
378
- - Save a generated document
379
- - Store a document in their document library
380
 
381
  Args:
382
- title: Document title (e.g., "Contract de bail", "Note juridique")
383
- content: Document content in HTML format (e.g., "<h1>Title</h1><p>Content...</p>")
384
- path: Folder path where to save the document
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 success status
391
 
392
  Example:
393
  create_draft_document(
394
- title="Contract de bail",
395
- content="<h1>Contrat de bail</h1><p>Ce contrat est conclu entre...</p>",
396
- path="Contracts/"
397
  )
398
- β†’ Saves as "./Contracts/Contract de bail.pdf" and returns confirmation
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
- Real implementation of create_draft_document - calls Supabase endpoint.
 
411
  Args:
412
- user_id: User UUID (injected by the agent)
413
  title: Document title
414
  content: Document HTML content
415
  path: Folder path
416
 
417
  Returns:
418
- Success/failure message with document path
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
- return f"βœ… Document successfully saved to: {full_path}"
 
 
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, create_draft_document]
945
- tools_for_client = [_query_knowledge_graph, _find_lawyers, _message_lawyer, search_web, _create_draft_document]
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