File size: 12,102 Bytes
6853143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8cc8e89
6853143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
#!/usr/bin/env python3
"""
Test the /doc_creator endpoint for TipTap JSON document editing
"""

import asyncio
import json
import sys
import os
from pathlib import Path
from dotenv import load_dotenv

# Load environment variables
load_dotenv(dotenv_path=".env", override=False)

# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent))

from structured_outputs.api_models import DocCreatorRequest, DocCreatorResponse, Message
from agents.doc_editor import DocumentEditorAgent
from langchain_openai import ChatOpenAI


# Sample TipTap document for testing
SAMPLE_TAPTAP_DOC = {
    "type": "doc",
    "content": [
        {
            "type": "heading",
            "attrs": {"level": 1, "textAlign": "center"},
            "content": [
                {"type": "text", "text": "CONTRAT DE PRESTATION DE SERVICES"}
            ]
        },
        {
            "type": "heading",
            "attrs": {"level": 2, "textAlign": "left"},
            "content": [
                {"type": "text", "text": "Article 1 - Objet"}
            ]
        },
        {
            "type": "paragraph",
            "attrs": {"textAlign": "justify"},
            "content": [
                {"type": "text", "text": "Le présent contrat a pour objet de définir les conditions dans lesquelles "},
                {"type": "text", "marks": [{"type": "bold"}], "text": "la Société X"},
                {"type": "text", "text": " s'engage à fournir des services."}
            ]
        },
        {
            "type": "heading",
            "attrs": {"level": 2, "textAlign": "left"},
            "content": [
                {"type": "text", "text": "Article 2 - Durée"}
            ]
        },
        {
            "type": "paragraph",
            "attrs": {"textAlign": "justify"},
            "content": [
                {"type": "text", "text": "Le contrat prend effet le "},
                {"type": "text", "marks": [{"type": "italic"}], "text": "1er janvier 2026"},
                {"type": "text", "text": " pour une durée de "},
                {"type": "text", "marks": [{"type": "underline"}], "text": "12 mois"},
                {"type": "text", "text": "."}
            ]
        }
    ]
}


async def test_endpoint_direct():
    """Test the doc_editor agent directly (simulating endpoint behavior)"""
    print("🧪 Testing /doc_creator endpoint behavior directly")
    print("=" * 60)
    
    # Initialize agent
    print("\n📝 Initializing DocumentEditorAgent...")
    llm_model = os.getenv("LLM_MODEL", "gpt-4o-mini")
    print(f"   Using model: {llm_model}")
    llm = ChatOpenAI(model=llm_model, temperature=0)
    doc_editor = DocumentEditorAgent(llm=llm)
    
    # Convert to canonical format (as endpoint does)
    doc_text = json.dumps(SAMPLE_TAPTAP_DOC, ensure_ascii=False, sort_keys=True, indent=2)
    
    print(f"✅ Canonical document prepared ({len(doc_text)} characters)")
    
    # Test 1: Simple replacement
    print("\n" + "=" * 60)
    print("🔧 TEST 1: Replace '12 mois' with '24 mois'")
    print("=" * 60)
    
    result1 = await doc_editor.edit_document(
        doc_text=doc_text,
        user_instruction="Change '12 mois' en '24 mois' dans la durée du contrat",
        doc_summaries=[],
        conversation_history=[],
        max_iterations=10
    )
    
    print(f"✅ Success: {result1['success']}")
    print(f"📝 Message: {result1['message']}")
    print(f"🔄 Iterations: {result1.get('iteration_count', 'N/A')}")
    
    if result1['success']:
        modified_doc = json.loads(result1['doc_text'])
        
        # Verify the change
        found_24 = False
        for item in modified_doc.get('content', []):
            if item.get('type') == 'paragraph':
                content = item.get('content', [])
                for text_node in content:
                    if text_node.get('type') == 'text':
                        if '24 mois' in text_node.get('text', ''):
                            found_24 = True
        
        print(f"✅ Verification: {'24 mois found' if found_24 else '24 mois NOT found'}")
    
    # Test 2: Add new article
    print("\n" + "=" * 60)
    print("🔧 TEST 2: Add Article 3 - Prix")
    print("=" * 60)
    
    current_doc_text = result1['doc_text'] if result1['success'] else doc_text
    
    result2 = await doc_editor.edit_document(
        doc_text=current_doc_text,
        user_instruction="Ajoute un Article 3 - Prix après l'Article 2. Le prix est de 200€ par heure.",
        doc_summaries=[],
        conversation_history=[],
        max_iterations=10
    )
    
    print(f"✅ Success: {result2['success']}")
    print(f"📝 Message: {result2['message']}")
    print(f"🔄 Iterations: {result2.get('iteration_count', 'N/A')}")
    
    if result2['success']:
        modified_doc = json.loads(result2['doc_text'])
        
        # Verify the article was added
        article3_count = 0
        for item in modified_doc.get('content', []):
            if item.get('type') == 'heading':
                content = item.get('content', [])
                for text_node in content:
                    if text_node.get('type') == 'text':
                        if 'Article 3' in text_node.get('text', '') and 'Prix' in text_node.get('text', ''):
                            article3_count += 1
        
        print(f"✅ Verification: {'Article 3 found' if article3_count > 0 else 'Article 3 NOT found'}")
    
    # Test 3: Delete content
    print("\n" + "=" * 60)
    print("🔧 TEST 3: Remove 'la Société X' and replace with 'CyberLGL'")
    print("=" * 60)
    
    current_doc_text = result2['doc_text'] if result2['success'] else current_doc_text
    
    result3 = await doc_editor.edit_document(
        doc_text=current_doc_text,
        user_instruction="Remplace 'la Société X' par 'CyberLGL' dans l'Article 1",
        doc_summaries=[],
        conversation_history=[],
        max_iterations=10
    )
    
    print(f"✅ Success: {result3['success']}")
    print(f"📝 Message: {result3['message']}")
    print(f"🔄 Iterations: {result3.get('iteration_count', 'N/A')}")
    
    if result3['success']:
        modified_doc = json.loads(result3['doc_text'])
        
        # Verify the change
        found_cyberlgl = False
        for item in modified_doc.get('content', []):
            if item.get('type') == 'paragraph':
                content = item.get('content', [])
                for text_node in content:
                    if text_node.get('type') == 'text':
                        if 'CyberLGL' in text_node.get('text', ''):
                            found_cyberlgl = True
        
        print(f"✅ Verification: {'CyberLGL found' if found_cyberlgl else 'CyberLGL NOT found'}")
    
    # Test 4: Complex edit with context
    print("\n" + "=" * 60)
    print("🔧 TEST 4: Add confidentiality clause with document context")
    print("=" * 60)
    
    current_doc_text = result3['doc_text'] if result3['success'] else current_doc_text
    
    doc_summaries = [
        {
            "file_name": "contrat_type.pdf",
            "summary": "Contrat type avec clause de confidentialité standard",
            "actors": "Prestataire, Client",
            "key_details": "Clause de confidentialité de 5 ans après fin du contrat"
        }
    ]
    
    result4 = await doc_editor.edit_document(
        doc_text=current_doc_text,
        user_instruction="Ajoute une clause de confidentialité conforme au contrat type après l'Article 3",
        doc_summaries=doc_summaries,
        conversation_history=[],
        max_iterations=10
    )
    
    print(f"✅ Success: {result4['success']}")
    print(f"📝 Message: {result4['message']}")
    print(f"🔄 Iterations: {result4.get('iteration_count', 'N/A')}")
    
    if result4['success']:
        modified_doc = json.loads(result4['doc_text'])
        
        # Count articles
        article_count = 0
        for item in modified_doc.get('content', []):
            if item.get('type') == 'heading':
                content = item.get('content', [])
                for text_node in content:
                    if text_node.get('type') == 'text':
                        if 'Article' in text_node.get('text', ''):
                            article_count += 1
        
        print(f"✅ Verification: Total articles = {article_count}")
    
    # Summary
    print("\n" + "=" * 60)
    print("📊 TEST SUMMARY")
    print("=" * 60)
    tests = [
        ("Replace text", result1['success']),
        ("Add article", result2['success']),
        ("Modify content", result3['success']),
        ("Add with context", result4['success'])
    ]
    
    for test_name, success in tests:
        status = "✅ PASS" if success else "❌ FAIL"
        print(f"{status} - {test_name}")
    
    total_tests = len(tests)
    passed_tests = sum(1 for _, success in tests if success)
    print(f"\n📈 Results: {passed_tests}/{total_tests} tests passed")
    
    return all(success for _, success in tests)


async def test_request_format():
    """Test that the request/response format matches DocCreatorRequest/DocCreatorResponse"""
    print("\n" + "=" * 60)
    print("🧪 Testing Request/Response Format")
    print("=" * 60)
    
    # Create DocCreatorRequest
    request = DocCreatorRequest(
        instruction="Test instruction",
        documentContent=SAMPLE_TAPTAP_DOC,
        contentFormat="tiptap-json",
        documentSummaries=None,
        conversationHistory=[
            Message(role="user", content="Previous message"),
            Message(role="assistant", content="Previous response")
        ],
        clientId="test-client-123"
    )
    
    print(f"✅ DocCreatorRequest created")
    print(f"   - instruction: {request.instruction}")
    print(f"   - contentFormat: {request.contentFormat}")
    print(f"   - clientId: {request.clientId}")
    print(f"   - conversationHistory length: {len(request.conversationHistory)}")
    
    # Create DocCreatorResponse
    response = DocCreatorResponse(
        response="Test successful",
        modifiedDocument=SAMPLE_TAPTAP_DOC,
        processing_time=1.5,
        timestamp="2026-02-15T19:00:00",
        error=None
    )
    
    print(f"\n✅ DocCreatorResponse created")
    print(f"   - response: {response.response}")
    print(f"   - processing_time: {response.processing_time}")
    print(f"   - error: {response.error}")
    
    return True


def print_canonical_format():
    """Show what the canonical format looks like"""
    print("\n" + "=" * 60)
    print("📋 Canonical TipTap JSON Format")
    print("=" * 60)
    
    canonical = json.dumps(SAMPLE_TAPTAP_DOC, ensure_ascii=False, sort_keys=True, indent=2)
    print(canonical)
    print(f"\n📊 Format info:")
    print(f"   - Characters: {len(canonical)}")
    print(f"   - Lines: {len(canonical.split(chr(10)))}")
    print(f"   - Keys sorted: Yes (sort_keys=True)")
    print(f"   - Indentation: 2 spaces")


async def main():
    """Run all tests"""
    print("🚀 Document Editor Endpoint Test Suite")
    print("=" * 60)
    
    try:
        # Show canonical format
        print_canonical_format()
        
        # Test request/response format
        await test_request_format()
        
        # Test endpoint behavior
        success = await test_endpoint_direct()
        
        # Final summary
        print("\n" + "=" * 60)
        print("✅ ALL TESTS COMPLETED")
        print("=" * 60)
        
        if success:
            print("🎉 The /doc_creator endpoint is ready for use!")
            print("\n📖 See DOC_CREATOR_ENDPOINT_GUIDE.md for usage instructions")
        else:
            print("⚠️  Some tests failed. Check the output above for details.")
        
        return 0 if success else 1
        
    except Exception as e:
        print(f"\n❌ Test suite failed with error: {e}")
        import traceback
        traceback.print_exc()
        return 1


if __name__ == "__main__":
    exit_code = asyncio.run(main())
    sys.exit(exit_code)