Charles Grandjean commited on
Commit
2f4c4da
·
1 Parent(s): 47fa0a5

tree format in prompt

Browse files
agent_api.py CHANGED
@@ -20,7 +20,8 @@ import secrets
20
  from structured_outputs.api_models import (
21
  Message, DocumentAnalysis, ChatRequest, ChatResponse,
22
  HealthResponse, AnalyzePDFRequest, AnalyzePDFResponse,
23
- LawyerProfile, DocCreatorRequest, DocCreatorResponse
 
24
  )
25
 
26
  from langgraph_agent import CyberLegalAgent
@@ -136,7 +137,60 @@ class CyberLegalAPI:
136
  self.conversation_manager = ConversationManager()
137
  logger.info(f"🔧 CyberLegalAPI initialized with {llm_provider.upper()} provider")
138
 
139
- def _build_lawyer_prompt(self, document_analyses: Optional[List[DocumentAnalysis]], jurisdiction: str, lawyer_profile: Optional[LawyerProfile] = None) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  """Build lawyer prompt with optional document context and lawyer profile"""
141
  prompt_parts = []
142
 
@@ -158,16 +212,11 @@ class CyberLegalAPI:
158
  profile_text += "\nWhen answering, consider this lawyer's expertise and experience level. Tailor your responses to be appropriate for their seniority and specialization.\n"
159
  prompt_parts.append(profile_text)
160
 
161
- # Add document analyses if available
162
- if document_analyses:
163
- docs_text = "\n### Documents parsed in the lawyer profile\n"
164
- for i, doc in enumerate(document_analyses, 1):
165
- docs_text += f"[Doc {i}] {doc.file_name}\n"
166
- if doc.summary: docs_text += f"Summary: {doc.summary}\n"
167
- if doc.actors: docs_text += f"Actors: {doc.actors}\n"
168
- if doc.key_details: docs_text += f"Key Details: {doc.key_details}\n"
169
- docs_text += "\n"
170
- docs_text += "Use these documents if the user's question is related to their content.\n"
171
  prompt_parts.append(docs_text)
172
 
173
  # Combine base prompt with context
@@ -208,18 +257,20 @@ class CyberLegalAPI:
208
  logger.info(f"💬 User query: {request.message}")
209
 
210
  try:
211
- # Build dynamic system prompt for lawyers with document analyses and/or lawyer profile
212
  if request.userType == "lawyer":
213
  system_prompt = self._build_lawyer_prompt(
214
- request.documentAnalyses,
215
  request.jurisdiction,
216
  request.lawyerProfile
217
  )
218
  context_parts = []
219
  if request.lawyerProfile:
220
  context_parts.append("lawyer profile")
221
- if request.documentAnalyses:
222
- context_parts.append(f"{len(request.documentAnalyses)} document analyses")
 
 
223
  if context_parts:
224
  logger.info(f"📚 Using lawyer prompt with {', '.join(context_parts)}")
225
  else:
@@ -320,6 +371,7 @@ class CyberLegalAPI:
320
  actors=result.get("actors", ""),
321
  key_details=result.get("key_details", ""),
322
  summary=result.get("summary", ""),
 
323
  processing_status=result.get("processing_status", "unknown"),
324
  processing_time=processing_time,
325
  timestamp=datetime.now().isoformat(),
@@ -366,7 +418,12 @@ class CyberLegalAPI:
366
  logger.info(f"👤 Client ID: {request.clientId}")
367
  logger.info(f"📋 Instruction: {request.instruction}")
368
  logger.info(f"📏 Document size: {len(request.documentContent)} bytes")
369
- logger.info(f"📚 Document summaries: {len(request.documentSummaries) if request.documentSummaries else 0}")
 
 
 
 
 
370
  logger.info(f"💬 Conversation history: {len(request.conversationHistory) if request.conversationHistory else 0} messages")
371
 
372
  try:
@@ -375,21 +432,16 @@ class CyberLegalAPI:
375
  doc_text = request.documentContent
376
  logger.info(f"✅ HTML document ready - size: {len(doc_text)} bytes")
377
 
378
- # Convert document summaries if provided
379
  doc_summaries = []
380
- if request.documentSummaries:
381
- logger.info("📚 Processing document summaries...")
382
- for i, doc in enumerate(request.documentSummaries, 1):
383
- logger.info(f" [{i}] {doc.file_name} - {doc.summary[:50]}...")
384
- doc_summaries.append({
385
- "file_name": doc.file_name,
386
- "summary": doc.summary,
387
- "actors": doc.actors,
388
- "key_details": doc.key_details
389
- })
390
- logger.info(f"✅ {len(doc_summaries)} document summaries loaded")
391
  else:
392
- logger.info("ℹ️ No document summaries provided")
393
 
394
  # Convert conversation history if provided
395
  conversation_history = []
 
20
  from structured_outputs.api_models import (
21
  Message, DocumentAnalysis, ChatRequest, ChatResponse,
22
  HealthResponse, AnalyzePDFRequest, AnalyzePDFResponse,
23
+ LawyerProfile, DocCreatorRequest, DocCreatorResponse,
24
+ DocumentsTree, TreeNode
25
  )
26
 
27
  from langgraph_agent import CyberLegalAgent
 
137
  self.conversation_manager = ConversationManager()
138
  logger.info(f"🔧 CyberLegalAPI initialized with {llm_provider.upper()} provider")
139
 
140
+ def _format_documents_tree(self, node: TreeNode, indent: int = 0) -> str:
141
+ """
142
+ Format documents tree as hierarchical text with indentation.
143
+
144
+ Example:
145
+ - Subdirectory 1:
146
+ - file11: summary | actors | key_details
147
+ - Sub-sub-directory 11:
148
+ - file111: summary | actors | key_details
149
+ """
150
+ lines = []
151
+ indent_str = " " * indent
152
+
153
+ if node.type == "folder":
154
+ lines.append(f"{indent_str}- {node.name}:")
155
+ if node.children:
156
+ for child in node.children:
157
+ lines.append(self._format_documents_tree(child, indent + 3))
158
+ elif node.type == "file" and node.analysis:
159
+ analysis_parts = []
160
+ if node.analysis.summary:
161
+ summary_preview = node.analysis.summary[:100] + "..." if len(node.analysis.summary) > 100 else node.analysis.summary
162
+ analysis_parts.append(f"summary: {summary_preview}")
163
+ if node.analysis.actors:
164
+ actors_preview = node.analysis.actors[:100] + "..." if len(node.analysis.actors) > 100 else node.analysis.actors
165
+ analysis_parts.append(f"actors: {actors_preview}")
166
+ if node.analysis.key_details:
167
+ details_preview = node.analysis.key_details[:100] + "..." if len(node.analysis.key_details) > 100 else node.analysis.key_details
168
+ analysis_parts.append(f"key_details: {details_preview}")
169
+
170
+ analysis_text = " | ".join(analysis_parts) if analysis_parts else "No analysis available"
171
+ lines.append(f"{indent_str}- {node.name}: {analysis_text}")
172
+
173
+ return "\n".join(lines)
174
+
175
+ def _extract_flat_documents(self, node: TreeNode) -> List[Dict[str, Any]]:
176
+ """
177
+ Recursively extract all documents with analysis from tree into flat list.
178
+ Used for endpoints that expect flat document structure.
179
+ """
180
+ docs = []
181
+ if node.type == "file" and node.analysis:
182
+ docs.append({
183
+ "file_name": node.name,
184
+ "summary": node.analysis.summary,
185
+ "actors": node.analysis.actors,
186
+ "key_details": node.analysis.key_details
187
+ })
188
+ if node.children:
189
+ for child in node.children:
190
+ docs.extend(self._extract_flat_documents(child))
191
+ return docs
192
+
193
+ def _build_lawyer_prompt(self, documents_tree: Optional[DocumentsTree], jurisdiction: str, lawyer_profile: Optional[LawyerProfile] = None) -> str:
194
  """Build lawyer prompt with optional document context and lawyer profile"""
195
  prompt_parts = []
196
 
 
212
  profile_text += "\nWhen answering, consider this lawyer's expertise and experience level. Tailor your responses to be appropriate for their seniority and specialization.\n"
213
  prompt_parts.append(profile_text)
214
 
215
+ # Add documents tree if available
216
+ if documents_tree and documents_tree.children:
217
+ docs_text = "\n### Documents in Lawyer's Database\n"
218
+ docs_text += self._format_documents_tree(documents_tree)
219
+ docs_text += "\n\nUse these documents when relevant to the question.\n"
 
 
 
 
 
220
  prompt_parts.append(docs_text)
221
 
222
  # Combine base prompt with context
 
257
  logger.info(f"💬 User query: {request.message}")
258
 
259
  try:
260
+ # Build dynamic system prompt for lawyers with documents tree and/or lawyer profile
261
  if request.userType == "lawyer":
262
  system_prompt = self._build_lawyer_prompt(
263
+ request.documents_tree,
264
  request.jurisdiction,
265
  request.lawyerProfile
266
  )
267
  context_parts = []
268
  if request.lawyerProfile:
269
  context_parts.append("lawyer profile")
270
+ if request.documents_tree and request.documents_tree.children:
271
+ # Count documents in tree
272
+ doc_count = sum(1 for node in self._extract_flat_documents(request.documents_tree))
273
+ context_parts.append(f"{doc_count} documents")
274
  if context_parts:
275
  logger.info(f"📚 Using lawyer prompt with {', '.join(context_parts)}")
276
  else:
 
371
  actors=result.get("actors", ""),
372
  key_details=result.get("key_details", ""),
373
  summary=result.get("summary", ""),
374
+ extracted_text=result.get("extracted_text", ""),
375
  processing_status=result.get("processing_status", "unknown"),
376
  processing_time=processing_time,
377
  timestamp=datetime.now().isoformat(),
 
418
  logger.info(f"👤 Client ID: {request.clientId}")
419
  logger.info(f"📋 Instruction: {request.instruction}")
420
  logger.info(f"📏 Document size: {len(request.documentContent)} bytes")
421
+
422
+ # Count documents in tree
423
+ doc_count = 0
424
+ if request.documents_tree and request.documents_tree.children:
425
+ doc_count = sum(1 for node in self._extract_flat_documents(request.documents_tree))
426
+ logger.info(f"📚 Documents in tree: {doc_count}")
427
  logger.info(f"💬 Conversation history: {len(request.conversationHistory) if request.conversationHistory else 0} messages")
428
 
429
  try:
 
432
  doc_text = request.documentContent
433
  logger.info(f"✅ HTML document ready - size: {len(doc_text)} bytes")
434
 
435
+ # Extract documents from tree if provided (convert to flat list for doc_editor agent)
436
  doc_summaries = []
437
+ if request.documents_tree and request.documents_tree.children:
438
+ logger.info("📚 Processing documents from tree...")
439
+ doc_summaries = self._extract_flat_documents(request.documents_tree)
440
+ for i, doc in enumerate(doc_summaries, 1):
441
+ logger.info(f" [{i}] {doc['file_name']} - {doc['summary'][:50] if doc['summary'] else 'No summary'}...")
442
+ logger.info(f" {len(doc_summaries)} documents loaded from tree")
 
 
 
 
 
443
  else:
444
+ logger.info("ℹ️ No documents provided")
445
 
446
  # Convert conversation history if provided
447
  conversation_history = []
structured_outputs/api_models.py CHANGED
@@ -25,13 +25,31 @@ class LawyerProfile(BaseModel):
25
 
26
 
27
  class DocumentAnalysis(BaseModel):
28
- """Document analysis result"""
29
- file_name: str
30
  summary: Optional[str] = None
31
  actors: Optional[str] = None
32
  key_details: Optional[str] = None
33
 
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  class ChatRequest(BaseModel):
36
  """Chat request model"""
37
  clientId: str = Field(..., description="Unique client identifier")
@@ -39,7 +57,7 @@ class ChatRequest(BaseModel):
39
  conversationHistory: Optional[List[Message]] = Field(default=[], description="Previous conversation messages")
40
  userType: Optional[str] = Field(default="client", description="User type: 'client' for general users or 'lawyer' for legal professionals")
41
  jurisdiction: Optional[str] = Field(default="Romania", description="Jurisdiction of the user")
42
- documentAnalyses: Optional[List[DocumentAnalysis]] = Field(default=None, description="Lawyer's document analyses")
43
  lawyerProfile: Optional[LawyerProfile] = Field(default=None, description="Lawyer's professional profile")
44
 
45
 
@@ -75,14 +93,14 @@ class AnalyzePDFResponse(BaseModel):
75
  processing_time: float = Field(..., description="Processing time in seconds")
76
  timestamp: str = Field(..., description="Analysis timestamp")
77
  error: Optional[str] = Field(None, description="Error message if any")
78
-
79
 
80
  class DocCreatorRequest(BaseModel):
81
  """Document creator request model"""
82
  instruction: str = Field(..., description="User's instruction for document editing")
83
  documentContent: str = Field(..., description="HTML document content")
84
  contentFormat: str = Field(default="html", description="Format of document content (always 'html')")
85
- documentSummaries: Optional[List[DocumentAnalysis]] = Field(default=None, description="Context from analyzed documents")
86
  conversationHistory: Optional[List[Message]] = Field(default=[], description="Previous conversation messages")
87
  clientId: str = Field(..., description="Unique client identifier")
88
 
 
25
 
26
 
27
  class DocumentAnalysis(BaseModel):
28
+ """Document analysis content"""
 
29
  summary: Optional[str] = None
30
  actors: Optional[str] = None
31
  key_details: Optional[str] = None
32
 
33
 
34
+ class TreeNode(BaseModel):
35
+ """Node in the document tree (folder or file)"""
36
+ name: str
37
+ type: str = Field(..., description="Type: 'folder' or 'file'")
38
+ file_path: Optional[str] = None
39
+ file_size: Optional[int] = None
40
+ mime_type: Optional[str] = None
41
+ created_at: Optional[str] = None
42
+ analysis: Optional[DocumentAnalysis] = None
43
+ children: Optional[List['TreeNode']] = None
44
+
45
+
46
+ class DocumentsTree(BaseModel):
47
+ """Root of the document tree"""
48
+ name: str = "root"
49
+ type: str = "folder"
50
+ children: Optional[List[TreeNode]] = None
51
+
52
+
53
  class ChatRequest(BaseModel):
54
  """Chat request model"""
55
  clientId: str = Field(..., description="Unique client identifier")
 
57
  conversationHistory: Optional[List[Message]] = Field(default=[], description="Previous conversation messages")
58
  userType: Optional[str] = Field(default="client", description="User type: 'client' for general users or 'lawyer' for legal professionals")
59
  jurisdiction: Optional[str] = Field(default="Romania", description="Jurisdiction of the user")
60
+ documents_tree: Optional[DocumentsTree] = Field(default=None, description="Hierarchical tree of lawyer's documents")
61
  lawyerProfile: Optional[LawyerProfile] = Field(default=None, description="Lawyer's professional profile")
62
 
63
 
 
93
  processing_time: float = Field(..., description="Processing time in seconds")
94
  timestamp: str = Field(..., description="Analysis timestamp")
95
  error: Optional[str] = Field(None, description="Error message if any")
96
+ extracted_content: Optional[str] = Field(None, description="Extracted content")
97
 
98
  class DocCreatorRequest(BaseModel):
99
  """Document creator request model"""
100
  instruction: str = Field(..., description="User's instruction for document editing")
101
  documentContent: str = Field(..., description="HTML document content")
102
  contentFormat: str = Field(default="html", description="Format of document content (always 'html')")
103
+ documents_tree: Optional[DocumentsTree] = Field(default=None, description="Hierarchical tree of documents for context")
104
  conversationHistory: Optional[List[Message]] = Field(default=[], description="Previous conversation messages")
105
  clientId: str = Field(..., description="Unique client identifier")
106
 
tests/test_documents_tree.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test the documents_tree integration with the API
4
+ """
5
+
6
+ import sys
7
+ import os
8
+
9
+ # Add parent directory to path
10
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
11
+
12
+ from structured_outputs.api_models import (
13
+ TreeNode, DocumentsTree, DocumentAnalysis, ChatRequest
14
+ )
15
+
16
+ def test_tree_node_creation():
17
+ """Test creating a tree node"""
18
+ print("🧪 Testing TreeNode creation...")
19
+
20
+ file_analysis = DocumentAnalysis(
21
+ summary="Test summary",
22
+ actors="Actor 1, Actor 2",
23
+ key_details="Key detail 1, Key detail 2"
24
+ )
25
+
26
+ file_node = TreeNode(
27
+ name="test.pdf",
28
+ type="file",
29
+ file_path="user-id/test.pdf",
30
+ file_size=100000,
31
+ mime_type="application/pdf",
32
+ created_at="2025-01-15T10:30:00Z",
33
+ analysis=file_analysis
34
+ )
35
+
36
+ assert file_node.name == "test.pdf"
37
+ assert file_node.type == "file"
38
+ assert file_node.analysis.summary == "Test summary"
39
+ print("✅ TreeNode creation works")
40
+
41
+ def test_documents_tree_creation():
42
+ """Test creating a documents tree"""
43
+ print("\n🧪 Testing DocumentsTree creation...")
44
+
45
+ # Create file nodes with analyses
46
+ file1_analysis = DocumentAnalysis(
47
+ summary="Contract summary",
48
+ actors="SCI Martin, SARL Dupont",
49
+ key_details="Durée: 9 ans, Loyer: 3500€/mois"
50
+ )
51
+
52
+ file1 = TreeNode(
53
+ name="bail-commercial.pdf",
54
+ type="file",
55
+ file_path="user-id/abc123-bail-commercial.pdf",
56
+ file_size=245000,
57
+ mime_type="application/pdf",
58
+ created_at="2025-01-15T10:30:00Z",
59
+ analysis=file1_analysis
60
+ )
61
+
62
+ file2_analysis = DocumentAnalysis(
63
+ summary="Legal note summary",
64
+ actors="Entreprise XYZ, CNIL",
65
+ key_details="Base légale: intérêt légitime"
66
+ )
67
+
68
+ file2 = TreeNode(
69
+ name="note-juridique.pdf",
70
+ type="file",
71
+ file_path="user-id/def456-note-juridique.pdf",
72
+ file_size=120000,
73
+ mime_type="application/pdf",
74
+ created_at="2025-02-01T14:00:00Z",
75
+ analysis=file2_analysis
76
+ )
77
+
78
+ # Create folder with files
79
+ contracts_folder = TreeNode(
80
+ name="Contracts",
81
+ type="folder",
82
+ children=[file1]
83
+ )
84
+
85
+ # Create root tree
86
+ tree = DocumentsTree(
87
+ name="root",
88
+ type="folder",
89
+ children=[contracts_folder, file2]
90
+ )
91
+
92
+ assert tree.name == "root"
93
+ assert tree.type == "folder"
94
+ assert len(tree.children) == 2
95
+ assert tree.children[0].name == "Contracts"
96
+ assert tree.children[0].type == "folder"
97
+ assert tree.children[0].children[0].name == "bail-commercial.pdf"
98
+ assert tree.children[1].name == "note-juridique.pdf"
99
+ print("✅ DocumentsTree creation works")
100
+
101
+ def test_chat_request_with_tree():
102
+ """Test creating a ChatRequest with documents_tree"""
103
+ print("\n🧪 Testing ChatRequest with documents_tree...")
104
+
105
+ # Create a simple tree
106
+ file_analysis = DocumentAnalysis(
107
+ summary="Test document",
108
+ actors="Actor 1",
109
+ key_details="Detail 1"
110
+ )
111
+
112
+ file_node = TreeNode(
113
+ name="test.pdf",
114
+ type="file",
115
+ analysis=file_analysis
116
+ )
117
+
118
+ tree = DocumentsTree(
119
+ children=[file_node]
120
+ )
121
+
122
+ # Create chat request
123
+ request = ChatRequest(
124
+ clientId="test-client",
125
+ message="What are my documents?",
126
+ userType="lawyer",
127
+ jurisdiction="Romania",
128
+ documents_tree=tree
129
+ )
130
+
131
+ assert request.clientId == "test-client"
132
+ assert request.message == "What are my documents?"
133
+ assert request.userType == "lawyer"
134
+ assert request.documents_tree is not None
135
+ assert request.documents_tree.children[0].name == "test.pdf"
136
+ print("✅ ChatRequest with documents_tree works")
137
+
138
+ def test_format_documents_tree():
139
+ """Test the _format_documents_tree method"""
140
+ print("\n🧪 Testing _format_documents_tree...")
141
+
142
+ # Import the API to access the method
143
+ from agent_api import CyberLegalAPI
144
+
145
+ # Create a tree
146
+ file_analysis = DocumentAnalysis(
147
+ summary="A very long summary that should be truncated at 100 characters and then show ellipsis",
148
+ actors="Actor 1, Actor 2, Actor 3",
149
+ key_details="Key detail"
150
+ )
151
+
152
+ file_node = TreeNode(
153
+ name="test.pdf",
154
+ type="file",
155
+ analysis=file_analysis
156
+ )
157
+
158
+ tree = DocumentsTree(
159
+ children=[file_node]
160
+ )
161
+
162
+ # Create API instance
163
+ api = CyberLegalAPI()
164
+
165
+ # Format the tree
166
+ formatted = api._format_documents_tree(tree)
167
+
168
+ print("📄 Formatted tree:")
169
+ print(formatted)
170
+
171
+ assert "test.pdf" in formatted
172
+ assert "summary:" in formatted
173
+ assert "actors:" in formatted
174
+ assert "key_details:" in formatted
175
+ print("✅ _format_documents_tree works")
176
+
177
+ def test_extract_flat_documents():
178
+ """Test the _extract_flat_documents method"""
179
+ print("\n🧪 Testing _extract_flat_documents...")
180
+
181
+ # Import the API to access the method
182
+ from agent_api import CyberLegalAPI
183
+
184
+ # Create a tree with multiple files in folders
185
+ file1_analysis = DocumentAnalysis(
186
+ summary="File 1 summary",
187
+ actors="Actor 1",
188
+ key_details="Detail 1"
189
+ )
190
+
191
+ file1 = TreeNode(
192
+ name="file1.pdf",
193
+ type="file",
194
+ analysis=file1_analysis
195
+ )
196
+
197
+ file2_analysis = DocumentAnalysis(
198
+ summary="File 2 summary",
199
+ actors="Actor 2",
200
+ key_details="Detail 2"
201
+ )
202
+
203
+ file2 = TreeNode(
204
+ name="file2.pdf",
205
+ type="file",
206
+ analysis=file2_analysis
207
+ )
208
+
209
+ folder = TreeNode(
210
+ name="Folder",
211
+ type="folder",
212
+ children=[file1]
213
+ )
214
+
215
+ tree = DocumentsTree(
216
+ children=[folder, file2]
217
+ )
218
+
219
+ # Create API instance
220
+ api = CyberLegalAPI()
221
+
222
+ # Extract flat documents
223
+ flat_docs = api._extract_flat_documents(tree)
224
+
225
+ print(f"📄 Extracted {len(flat_docs)} documents:")
226
+ for doc in flat_docs:
227
+ print(f" - {doc['file_name']}: {doc['summary']}")
228
+
229
+ assert len(flat_docs) == 2
230
+ assert flat_docs[0]['file_name'] == 'file1.pdf'
231
+ assert flat_docs[0]['summary'] == 'File 1 summary'
232
+ assert flat_docs[1]['file_name'] == 'file2.pdf'
233
+ assert flat_docs[1]['summary'] == 'File 2 summary'
234
+ print("✅ _extract_flat_documents works")
235
+
236
+ if __name__ == "__main__":
237
+ print("🚀 Running documents_tree integration tests\n")
238
+ print("=" * 80)
239
+
240
+ try:
241
+ test_tree_node_creation()
242
+ test_documents_tree_creation()
243
+ test_chat_request_with_tree()
244
+ test_format_documents_tree()
245
+ test_extract_flat_documents()
246
+
247
+ print("\n" + "=" * 80)
248
+ print("✅ All tests passed!")
249
+ print("=" * 80)
250
+
251
+ except Exception as e:
252
+ print("\n" + "=" * 80)
253
+ print(f"❌ Test failed: {e}")
254
+ print("=" * 80)
255
+ import traceback
256
+ traceback.print_exc()
257
+ sys.exit(1)