Sahil Garg commited on
Commit
2ac8811
Β·
1 Parent(s): 7e453aa

level-1 of generator and validator for notes

Browse files
Files changed (3) hide show
  1. SYSTEM_ARCHITECTURE_BOXES.md +139 -0
  2. agents/generator_validator.py +386 -0
  3. app.py +51 -19
SYSTEM_ARCHITECTURE_BOXES.md CHANGED
@@ -2421,3 +2421,142 @@ FILE DOWNLOAD
2421
  β”‚xlsx β”‚
2422
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
2423
  ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2421
  β”‚xlsx β”‚
2422
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
2423
  ```
2424
+
2425
+
2426
+
2427
+
2428
+
2429
+
2430
+ **generator-validator**
2431
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
2432
+ β”‚ API Request β”‚
2433
+ β”‚ POST /notes-llmβ”‚
2434
+ β”‚ with file β”‚
2435
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
2436
+ β”‚
2437
+ β–Ό
2438
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
2439
+ β”‚ create_notes_ β”‚
2440
+ β”‚ pipeline() β”‚
2441
+ β”‚ β”‚
2442
+ β”‚ β†’ LLMNotesGen β”‚
2443
+ β”‚ β†’ NotesValidatorβ”‚
2444
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
2445
+ β”‚
2446
+ β–Ό
2447
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
2448
+ β”‚Generator-Validator│◄───│ Max 3 Attempts β”‚
2449
+ β”‚ Pipeline β”‚ β”‚ β”‚
2450
+ β”‚ β”‚ β”‚ β”‚
2451
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
2452
+ β”‚ β”‚ Attempt β”‚ β”‚ β”‚ No
2453
+ β”‚ β”‚ Counter=0 β”‚ β”‚ β–Ό
2454
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
2455
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ Return Best β”‚
2456
+ β”‚ β”‚ Result β”‚
2457
+ β–Ό β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
2458
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
2459
+ β”‚ Generate β”‚
2460
+ β”‚ (LLM) β”‚
2461
+ β”‚ β”‚
2462
+ β”‚ - Call langgraphβ”‚
2463
+ β”‚ - Use RLHF if β”‚
2464
+ β”‚ requested β”‚
2465
+ β”‚ - Track attempt β”‚
2466
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
2467
+ β”‚
2468
+ β–Ό
2469
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
2470
+ β”‚ Validate β”‚
2471
+ β”‚ Quality β”‚
2472
+ β”‚ β”‚
2473
+ β”‚ - File exists β”‚
2474
+ β”‚ - Size >1KB β”‚
2475
+ β”‚ - Metadata OK β”‚
2476
+ β”‚ - RLHF quality β”‚
2477
+ β”‚ - Score 0.0-1.0 β”‚
2478
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
2479
+ β”‚
2480
+ β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”
2481
+ β”‚ β”‚
2482
+ β–Ό β–Ό
2483
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
2484
+ β”‚ Valid? β”‚ β”‚ Invalid β”‚
2485
+ β”‚ Score β”‚ β”‚ Score β”‚
2486
+ β”‚ β‰₯0.6 β”‚ β”‚ <0.6 β”‚
2487
+ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”˜
2488
+ β”‚ β”‚
2489
+ β–Ό β–Ό
2490
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
2491
+ β”‚ Return β”‚ β”‚ Refine β”‚
2492
+ β”‚ Success β”‚ β”‚ & Retry β”‚
2493
+ β”‚ with β”‚ β”‚ β”‚
2494
+ β”‚ Metadata β”‚ β”‚ - Use β”‚
2495
+ β”‚ Headers β”‚ β”‚ feedbackβ”‚
2496
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”˜
2497
+ β”‚
2498
+ β–Ό
2499
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
2500
+ β”‚Increment β”‚
2501
+ β”‚ Attempt β”‚
2502
+ β”‚ Counter β”‚
2503
+ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”˜
2504
+ β”‚
2505
+ └─────────────┐
2506
+ β–Ό
2507
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
2508
+ β”‚ Continue to β”‚
2509
+ β”‚ Next Attempt β”‚
2510
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
2511
+
2512
+
2513
+ **refine and retry**
2514
+
2515
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
2516
+ β”‚ Attempt 1 β”‚
2517
+ β”‚ Generate β”‚
2518
+ β”‚ β†’ Validate β”‚
2519
+ β”‚ Score: 0.4 β”‚ ❌ FAIL (< 0.6)
2520
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
2521
+ β”‚
2522
+ β–Ό
2523
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
2524
+ β”‚ Refinement β”‚
2525
+ β”‚ Analysis β”‚
2526
+ β”‚ β”‚
2527
+ β”‚ Feedback: β”‚
2528
+ β”‚ - "Low quality" β”‚
2529
+ β”‚ - "Small file" β”‚
2530
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
2531
+ β”‚
2532
+ β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”
2533
+ β”‚ β”‚
2534
+ β–Ό β–Ό
2535
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
2536
+ β”‚Quality β”‚ β”‚Other β”‚
2537
+ β”‚Issue? β”‚ β”‚Issue? β”‚
2538
+ β”‚ β”‚ β”‚ β”‚
2539
+ β”‚"quality" β”‚ β”‚File size β”‚
2540
+ β”‚in feedbackβ”‚ β”‚Metadata β”‚
2541
+ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”˜
2542
+ β”‚ β”‚
2543
+ β–Ό β–Ό
2544
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
2545
+ β”‚Switch to β”‚ β”‚Simple β”‚
2546
+ β”‚RLHF β”‚ β”‚Retry β”‚
2547
+ β”‚Mode β”‚ β”‚ β”‚
2548
+ β”‚ β”‚ β”‚Use same β”‚
2549
+ β”‚use_rlhf= β”‚ β”‚config β”‚
2550
+ β”‚true β”‚ β”‚ β”‚
2551
+ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”˜
2552
+ β”‚ β”‚
2553
+ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
2554
+ β”‚
2555
+ β–Ό
2556
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
2557
+ β”‚ Attempt 2 β”‚
2558
+ β”‚ Generate β”‚
2559
+ β”‚ (Improved) β”‚
2560
+ β”‚ β†’ Validate β”‚
2561
+ β”‚ Score: 0.8 β”‚ βœ… SUCCESS (β‰₯ 0.6)
2562
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
agents/generator_validator.py ADDED
@@ -0,0 +1,386 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Generator-Validator Pattern Implementation for Financial Notes
3
+ Implements formal Generator-Validator pattern with iterative refinement
4
+ """
5
+ import os
6
+ import json
7
+ import logging
8
+ from abc import ABC, abstractmethod
9
+ from typing import Dict, Any, List, Optional, Tuple
10
+ from dataclasses import dataclass
11
+ from datetime import datetime
12
+ import subprocess
13
+ import shutil
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ @dataclass
18
+ class ValidationResult:
19
+ """Result of validation process"""
20
+ is_valid: bool
21
+ score: float
22
+ feedback: List[str]
23
+ suggestions: List[str]
24
+ metadata: Dict[str, Any]
25
+
26
+ @dataclass
27
+ class GenerationResult:
28
+ """Result of generation process"""
29
+ success: bool
30
+ output_path: Optional[str]
31
+ data: Optional[Dict[str, Any]]
32
+ error: Optional[str]
33
+ metadata: Dict[str, Any]
34
+
35
+ class BaseGenerator(ABC):
36
+ """Abstract base class for financial statement generators"""
37
+
38
+ def __init__(self, max_attempts: int = 3):
39
+ self.max_attempts = max_attempts
40
+ self.attempts_made = 0
41
+
42
+ @abstractmethod
43
+ def generate(self, file_path: str, **kwargs) -> GenerationResult:
44
+ """Generate financial statement from input file"""
45
+ pass
46
+
47
+ @abstractmethod
48
+ def refine(self, previous_result: GenerationResult, feedback: List[str]) -> GenerationResult:
49
+ """Refine generation based on validation feedback"""
50
+ pass
51
+
52
+ class BaseValidator(ABC):
53
+ """Abstract base class for financial statement validators"""
54
+
55
+ @abstractmethod
56
+ def validate(self, generation_result: GenerationResult) -> ValidationResult:
57
+ """Validate the generated financial statement"""
58
+ pass
59
+
60
+ @abstractmethod
61
+ def get_validation_criteria(self) -> List[str]:
62
+ """Return list of validation criteria"""
63
+ pass
64
+
65
+ class LLMNotesGenerator(BaseGenerator):
66
+ """Generator for AI-powered financial notes"""
67
+
68
+ def __init__(self, max_attempts: int = 3, use_rlhf: bool = False):
69
+ super().__init__(max_attempts)
70
+ self.use_rlhf = use_rlhf
71
+
72
+ def generate(self, file_path: str, **kwargs) -> GenerationResult:
73
+ """Generate notes using AI/LLM approach"""
74
+ try:
75
+ self.attempts_made += 1
76
+ execution_id = f"notes_llm_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{self.attempts_made}"
77
+
78
+ # Choose workflow based on RLHF preference
79
+ if self.use_rlhf:
80
+ from agents.rlhf_workflows import run_rlhf_workflow
81
+ result = run_rlhf_workflow(file_path, "notes-llm")
82
+ else:
83
+ from agents.langgraph import run_workflow
84
+ result = run_workflow(file_path, "notes-llm")
85
+
86
+ if result["status"] == "success":
87
+ return GenerationResult(
88
+ success=True,
89
+ output_path=result["result"]["output_xlsx_path"],
90
+ data=result["result"],
91
+ error=None,
92
+ metadata={
93
+ "execution_id": execution_id,
94
+ "generation_method": "llm",
95
+ "use_rlhf": self.use_rlhf,
96
+ "attempt": self.attempts_made,
97
+ "rlhf_metadata": result["result"].get("rlhf_metadata", {})
98
+ }
99
+ )
100
+ else:
101
+ return GenerationResult(
102
+ success=False,
103
+ output_path=None,
104
+ data=None,
105
+ error=result.get("error", "Unknown error"),
106
+ metadata={
107
+ "execution_id": execution_id,
108
+ "generation_method": "llm",
109
+ "use_rlhf": self.use_rlhf,
110
+ "attempt": self.attempts_made
111
+ }
112
+ )
113
+
114
+ except Exception as e:
115
+ logger.error(f"LLM Notes generation failed: {e}")
116
+ return GenerationResult(
117
+ success=False,
118
+ output_path=None,
119
+ data=None,
120
+ error=str(e),
121
+ metadata={
122
+ "execution_id": f"error_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
123
+ "generation_method": "llm",
124
+ "use_rlhf": self.use_rlhf,
125
+ "attempt": self.attempts_made
126
+ }
127
+ )
128
+
129
+ def refine(self, previous_result: GenerationResult, feedback: List[str]) -> GenerationResult:
130
+ """Refine LLM notes generation based on feedback"""
131
+ logger.info(f"Refining LLM notes generation with feedback: {feedback}")
132
+
133
+ # For LLM generation, we can try different approaches:
134
+ # 1. Switch to RLHF if not already using it
135
+ # 2. Retry with different parameters
136
+ # 3. Use fallback models
137
+
138
+ if not self.use_rlhf and "quality" in str(feedback).lower():
139
+ # If quality issues and not using RLHF, try RLHF
140
+ logger.info("Switching to RLHF for better quality")
141
+ original_rlhf = self.use_rlhf
142
+ self.use_rlhf = True
143
+ result = self.generate(previous_result.data.get("file_path") if previous_result.data else None)
144
+ self.use_rlhf = original_rlhf # Reset for future calls
145
+ return result
146
+ else:
147
+ # Otherwise, just retry
148
+ return self.generate(previous_result.data.get("file_path") if previous_result.data else None)
149
+ """Generator for AI-powered financial notes"""
150
+
151
+ def __init__(self, max_attempts: int = 3, use_rlhf: bool = False):
152
+ super().__init__(max_attempts)
153
+ self.use_rlhf = use_rlhf
154
+
155
+ def generate(self, file_path: str, **kwargs) -> GenerationResult:
156
+ """Generate notes using AI/LLM approach"""
157
+ try:
158
+ self.attempts_made += 1
159
+ execution_id = f"notes_llm_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{self.attempts_made}"
160
+
161
+ # Choose workflow based on RLHF preference
162
+ if self.use_rlhf:
163
+ from agents.rlhf_workflows import run_rlhf_workflow
164
+ result = run_rlhf_workflow(file_path, "notes-llm")
165
+ else:
166
+ from agents.langgraph import run_workflow
167
+ result = run_workflow(file_path, "notes-llm")
168
+
169
+ if result["status"] == "success":
170
+ return GenerationResult(
171
+ success=True,
172
+ output_path=result["result"]["output_xlsx_path"],
173
+ data=result["result"],
174
+ error=None,
175
+ metadata={
176
+ "execution_id": execution_id,
177
+ "generation_method": "llm",
178
+ "use_rlhf": self.use_rlhf,
179
+ "attempt": self.attempts_made,
180
+ "rlhf_metadata": result["result"].get("rlhf_metadata", {})
181
+ }
182
+ )
183
+ else:
184
+ return GenerationResult(
185
+ success=False,
186
+ output_path=None,
187
+ data=None,
188
+ error=result.get("error", "Unknown error"),
189
+ metadata={
190
+ "execution_id": execution_id,
191
+ "generation_method": "llm",
192
+ "use_rlhf": self.use_rlhf,
193
+ "attempt": self.attempts_made
194
+ }
195
+ )
196
+
197
+ except Exception as e:
198
+ logger.error(f"LLM Notes generation failed: {e}")
199
+ return GenerationResult(
200
+ success=False,
201
+ output_path=None,
202
+ data=None,
203
+ error=str(e),
204
+ metadata={
205
+ "execution_id": f"error_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
206
+ "generation_method": "llm",
207
+ "use_rlhf": self.use_rlhf,
208
+ "attempt": self.attempts_made
209
+ }
210
+ )
211
+
212
+ def refine(self, previous_result: GenerationResult, feedback: List[str]) -> GenerationResult:
213
+ """Refine LLM notes generation based on feedback"""
214
+ logger.info(f"Refining LLM notes generation with feedback: {feedback}")
215
+
216
+ # For LLM generation, we can try different approaches:
217
+ # 1. Switch to RLHF if not already using it
218
+ # 2. Retry with different parameters
219
+ # 3. Use fallback models
220
+
221
+ if not self.use_rlhf and "quality" in str(feedback).lower():
222
+ # If quality issues and not using RLHF, try RLHF
223
+ logger.info("Switching to RLHF for better quality")
224
+ original_rlhf = self.use_rlhf
225
+ self.use_rlhf = True
226
+ result = self.generate(previous_result.data.get("file_path") if previous_result.data else None)
227
+ self.use_rlhf = original_rlhf # Reset for future calls
228
+ return result
229
+ else:
230
+ # Otherwise, just retry
231
+ return self.generate(previous_result.data.get("file_path") if previous_result.data else None)
232
+
233
+ class NotesValidator(BaseValidator):
234
+ """Validator for financial notes quality"""
235
+
236
+ def validate(self, generation_result: GenerationResult) -> ValidationResult:
237
+ """Validate generated financial notes"""
238
+ if not generation_result.success or not generation_result.output_path:
239
+ return ValidationResult(
240
+ is_valid=False,
241
+ score=0.0,
242
+ feedback=["Generation failed - no output produced"],
243
+ suggestions=["Retry generation process"],
244
+ metadata={"validation_type": "basic"}
245
+ )
246
+
247
+ feedback = []
248
+ suggestions = []
249
+ score = 1.0 # Start with perfect score
250
+
251
+ try:
252
+ # Check if output file exists
253
+ if not os.path.exists(generation_result.output_path):
254
+ feedback.append("Output file does not exist")
255
+ suggestions.append("Check file generation process")
256
+ score -= 0.5
257
+
258
+ # Check file size (reasonable minimum)
259
+ if os.path.exists(generation_result.output_path):
260
+ file_size = os.path.getsize(generation_result.output_path)
261
+ if file_size < 1000: # Less than 1KB
262
+ feedback.append("Output file is unusually small")
263
+ suggestions.append("Verify data processing and Excel generation")
264
+ score -= 0.3
265
+
266
+ # Check for common financial notes elements
267
+ # This would require reading the Excel file, but for now we'll do basic checks
268
+
269
+ # Check metadata completeness
270
+ metadata = generation_result.metadata
271
+ if not metadata.get("execution_id"):
272
+ feedback.append("Missing execution ID in metadata")
273
+ score -= 0.1
274
+
275
+ if generation_result.metadata.get("generation_method") == "llm":
276
+ # Additional checks for LLM-generated content
277
+ rlhf_metadata = metadata.get("rlhf_metadata", {})
278
+ if rlhf_metadata and rlhf_metadata.get("predicted_quality", 0) < 0.5:
279
+ feedback.append("Low quality score from RLHF validation")
280
+ suggestions.append("Consider regenerating with RLHF enabled")
281
+ score -= 0.4
282
+
283
+ # Ensure score doesn't go below 0
284
+ score = max(0.0, score)
285
+
286
+ return ValidationResult(
287
+ is_valid=score >= 0.6, # 60% threshold for validity
288
+ score=score,
289
+ feedback=feedback,
290
+ suggestions=suggestions,
291
+ metadata={
292
+ "validation_type": "comprehensive",
293
+ "file_size": os.path.getsize(generation_result.output_path) if os.path.exists(generation_result.output_path) else 0,
294
+ "has_rlhf": bool(generation_result.metadata.get("rlhf_metadata"))
295
+ }
296
+ )
297
+
298
+ except Exception as e:
299
+ logger.error(f"Validation failed: {e}")
300
+ return ValidationResult(
301
+ is_valid=False,
302
+ score=0.0,
303
+ feedback=[f"Validation error: {str(e)}"],
304
+ suggestions=["Check system logs for detailed error information"],
305
+ metadata={"validation_type": "error"}
306
+ )
307
+
308
+ def get_validation_criteria(self) -> List[str]:
309
+ """Return list of validation criteria"""
310
+ return [
311
+ "Output file exists and is accessible",
312
+ "File size is reasonable (>1KB)",
313
+ "Metadata contains required fields",
314
+ "For LLM generation: quality score meets threshold",
315
+ "RLHF metadata present when RLHF is enabled",
316
+ "No critical errors in generation process"
317
+ ]
318
+
319
+ class GeneratorValidatorPipeline:
320
+ """Main pipeline that orchestrates Generator-Validator pattern"""
321
+
322
+ def __init__(self, generator: BaseGenerator, validator: BaseValidator):
323
+ self.generator = generator
324
+ self.validator = validator
325
+ self.generation_history = []
326
+ self.validation_history = []
327
+
328
+ def process(self, file_path: str, **kwargs) -> Tuple[GenerationResult, ValidationResult]:
329
+ """Process file through generator-validator pipeline"""
330
+ logger.info("Starting Generator-Validator pipeline")
331
+
332
+ best_result = None
333
+ best_validation = None
334
+
335
+ for attempt in range(self.generator.max_attempts):
336
+ logger.info(f"Attempt {attempt + 1}/{self.generator.max_attempts}")
337
+
338
+ # Generate
339
+ generation_result = self.generator.generate(file_path, **kwargs)
340
+ self.generation_history.append(generation_result)
341
+
342
+ # Validate
343
+ validation_result = self.validator.validate(generation_result)
344
+ self.validation_history.append(validation_result)
345
+
346
+ logger.info(f"Generation success: {generation_result.success}, Validation score: {validation_result.score}")
347
+
348
+ # Keep track of best result
349
+ if best_result is None or (generation_result.success and validation_result.score > (best_validation.score if best_validation else 0)):
350
+ best_result = generation_result
351
+ best_validation = validation_result
352
+
353
+ # If validation passes, return immediately
354
+ if validation_result.is_valid:
355
+ logger.info("Validation passed - returning result")
356
+ return generation_result, validation_result
357
+
358
+ # If not the last attempt, try to refine
359
+ if attempt < self.generator.max_attempts - 1:
360
+ logger.info(f"Validation failed - refining with feedback: {validation_result.feedback}")
361
+ # Reset attempts counter for refinement
362
+ original_attempts = self.generator.attempts_made
363
+ self.generator.attempts_made = 0 # Reset for refinement
364
+ generation_result = self.generator.refine(generation_result, validation_result.feedback)
365
+ self.generator.attempts_made = original_attempts
366
+
367
+ logger.info("All attempts completed - returning best result")
368
+ return best_result, best_validation
369
+
370
+ def get_processing_summary(self) -> Dict[str, Any]:
371
+ """Get summary of the processing pipeline"""
372
+ return {
373
+ "total_attempts": len(self.generation_history),
374
+ "successful_generations": sum(1 for g in self.generation_history if g.success),
375
+ "validation_scores": [v.score for v in self.validation_history],
376
+ "best_score": max([v.score for v in self.validation_history]) if self.validation_history else 0,
377
+ "generation_methods": list(set([g.metadata.get("generation_method") for g in self.generation_history if g.metadata])),
378
+ "validation_criteria": self.validator.get_validation_criteria()
379
+ }
380
+
381
+ def create_notes_pipeline(use_rlhf: bool = False) -> GeneratorValidatorPipeline:
382
+ """Factory function to create LLM-based pipeline for notes generation"""
383
+ generator = LLMNotesGenerator(use_rlhf=use_rlhf)
384
+ validator = NotesValidator()
385
+
386
+ return GeneratorValidatorPipeline(generator, validator)
app.py CHANGED
@@ -5,6 +5,7 @@ import os
5
  import shutil
6
  import logging
7
  import json
 
8
  from agents.langgraph import run_workflow
9
  from agents.rlhf_workflows import run_rlhf_workflow
10
  from agents.rlhf_routes import rlhf_router
@@ -37,29 +38,60 @@ router = APIRouter()
37
 
38
  @router.post("/notes-llm")
39
  async def notes_llm_route(file: UploadFile = File(...), use_rlhf: bool = Query(False)):
 
40
  file_path = f"data/input/{file.filename}"
41
  os.makedirs("data/input", exist_ok=True)
42
  with open(file_path, "wb") as buffer:
43
  shutil.copyfileobj(file.file, buffer)
44
-
45
- # Choose workflow based on RLHF preference - using "notes-llm" as statement type
46
- if use_rlhf:
47
- result = run_rlhf_workflow(file_path, "notes-llm")
48
- else:
49
- result = run_workflow(file_path, "notes-llm")
50
-
51
- if result["status"] == "success":
52
- response = FileResponse(result["result"]["output_xlsx_path"], filename=os.path.basename(result["result"]["output_xlsx_path"]))
53
-
54
- # Add RLHF metadata to headers if available
55
- if "rlhf_metadata" in result.get("result", {}):
56
- rlhf_data = result["result"]["rlhf_metadata"]
57
- response.headers["X-RLHF-Statement-ID"] = str(rlhf_data.get("statement_id", ""))
58
- response.headers["X-RLHF-Quality-Score"] = str(rlhf_data.get("predicted_quality", ""))
59
- response.headers["X-RLHF-Confidence"] = str(rlhf_data.get("confidence_score", ""))
60
-
61
- return response
62
- raise HTTPException(status_code=500, detail=result["error"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
 
65
  @router.post("/notes")
 
5
  import shutil
6
  import logging
7
  import json
8
+ from agents.generator_validator import create_notes_pipeline
9
  from agents.langgraph import run_workflow
10
  from agents.rlhf_workflows import run_rlhf_workflow
11
  from agents.rlhf_routes import rlhf_router
 
38
 
39
  @router.post("/notes-llm")
40
  async def notes_llm_route(file: UploadFile = File(...), use_rlhf: bool = Query(False)):
41
+ """Generate AI-powered financial notes using Generator-Validator pattern"""
42
  file_path = f"data/input/{file.filename}"
43
  os.makedirs("data/input", exist_ok=True)
44
  with open(file_path, "wb") as buffer:
45
  shutil.copyfileobj(file.file, buffer)
46
+
47
+ try:
48
+ # Create Generator-Validator pipeline for LLM notes
49
+ pipeline = create_notes_pipeline(use_rlhf=use_rlhf)
50
+
51
+ # Process through pipeline
52
+ generation_result, validation_result = pipeline.process(file_path)
53
+
54
+ # Log processing summary
55
+ summary = pipeline.get_processing_summary()
56
+ logger.info(f"LLM Notes Pipeline Summary: {summary}")
57
+
58
+ if generation_result.success and validation_result.is_valid:
59
+ response = FileResponse(
60
+ generation_result.output_path,
61
+ filename=os.path.basename(generation_result.output_path)
62
+ )
63
+
64
+ # Add comprehensive metadata to headers
65
+ response.headers["X-Generation-Method"] = "llm"
66
+ response.headers["X-Validation-Score"] = str(validation_result.score)
67
+ response.headers["X-Attempts-Made"] = str(generation_result.metadata.get("attempt", 1))
68
+ response.headers["X-Execution-ID"] = generation_result.metadata.get("execution_id", "")
69
+
70
+ # Add RLHF metadata if available
71
+ if use_rlhf and "rlhf_metadata" in generation_result.metadata:
72
+ rlhf_data = generation_result.metadata["rlhf_metadata"]
73
+ response.headers["X-RLHF-Statement-ID"] = str(rlhf_data.get("statement_id", ""))
74
+ response.headers["X-RLHF-Quality-Score"] = str(rlhf_data.get("predicted_quality", ""))
75
+ response.headers["X-RLHF-Confidence"] = str(rlhf_data.get("confidence_score", ""))
76
+
77
+ # Add validation feedback
78
+ if validation_result.feedback:
79
+ response.headers["X-Validation-Feedback"] = json.dumps(validation_result.feedback)
80
+
81
+ return response
82
+ else:
83
+ error_detail = {
84
+ "generation_error": generation_result.error,
85
+ "validation_feedback": validation_result.feedback,
86
+ "validation_score": validation_result.score,
87
+ "attempts_made": generation_result.metadata.get("attempt", 1),
88
+ "processing_summary": summary
89
+ }
90
+ raise HTTPException(status_code=500, detail=json.dumps(error_detail))
91
+
92
+ except Exception as e:
93
+ logger.error(f"LLM Notes pipeline failed: {e}")
94
+ raise HTTPException(status_code=500, detail=f"Pipeline processing failed: {str(e)}")
95
 
96
 
97
  @router.post("/notes")