Sahil Garg commited on
Commit
29ee329
·
1 Parent(s): c00e175

udf generation is dynamic, different files, udf application on json

Browse files
agents/generator_validator.py CHANGED
@@ -201,58 +201,99 @@ class InteractiveFeedbackManager:
201
  apply_detailed_depreciation = 'depreciation' in feedback_lower and 'asset' in feedback_lower
202
  apply_increase_detail = 'detail' in feedback_lower
203
 
204
- # Handle formula feedback specifically
205
  if feedback_type == 'formula':
206
  return self._generate_formula_udf(feedback_text, iteration)
 
 
 
 
 
 
207
 
208
- # Create properly formatted UDF code
209
- udf_code = f'''def apply_user_feedback_v{iteration}(notes_data, feedback_type='{feedback_type}'):
 
210
  """
211
- UDF generated from user feedback iteration {iteration}
212
  Original Feedback: {feedback_text}
213
- Type: {feedback_type}
214
  Generated: {datetime.now().isoformat()}
215
  """
216
- import pandas as pd
217
  import re
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
- # Apply feedback-based modifications
220
- if notes_data and isinstance(notes_data, dict):
221
- # Modify notes content based on feedback analysis
222
- for sheet_name, df in notes_data.items():
223
- if isinstance(df, pd.DataFrame):
224
- df_copy = df.copy()
225
-
226
- # Add detailed depreciation notes with asset categories
227
- if {apply_detailed_depreciation}:
228
- if 'depreciation' in sheet_name.lower() or 'fixed asset' in sheet_name.lower():
229
- if len(df.columns) >= 1:
230
- # Add detailed descriptions to the first column
231
- if df_copy.columns[0] in df_copy.columns:
232
- mask = df_copy.iloc[:, 0].astype(str).str.contains('depreciation|asset', case=False, na=False)
233
- df_copy.loc[mask, df_copy.columns[0]] = df_copy.loc[mask, df_copy.columns[0]].astype(str) + \\
234
- ' - Detailed breakdown by asset category including buildings, equipment, furniture, and motor vehicles'
235
-
236
- # Increase detail level for all notes
237
- if {apply_increase_detail}:
238
- if len(df.columns) >= 1 and df_copy.columns[0] in df_copy.columns:
239
- for idx in df_copy.index:
240
- if pd.notna(df_copy.iloc[idx, 0]):
241
- current_value = str(df_copy.iloc[idx, 0])
242
- if 'depreciation' in current_value.lower():
243
- df_copy.iloc[idx, 0] = current_value + ' (Systematic allocation of asset cost over useful life)'
244
- elif 'inventory' in current_value.lower():
245
- df_copy.iloc[idx, 0] = current_value + ' (Valued at lower of cost or net realizable value)'
246
- elif 'loans' in current_value.lower() or 'advances' in current_value.lower():
247
- df_copy.iloc[idx, 0] = current_value + ' (Long-term financial assets with repayment terms)'
248
-
249
- # Update the notes data with modified dataframe
250
- notes_data[sheet_name] = df_copy
251
-
252
  return notes_data
253
  '''
254
 
255
- return udf_code
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
  def _generate_formula_udf(self, feedback_text: str, iteration: int) -> str:
258
  """Generate UDF specifically for formula feedback"""
@@ -284,61 +325,46 @@ class InteractiveFeedbackManager:
284
  """
285
  UDF generated from formula feedback iteration {iteration}
286
  Original Feedback: {feedback_text}
287
- Formula: Total = {operand1} - {operand2}
288
  Generated: {datetime.now().isoformat()}
289
  """
290
- import pandas as pd
291
-
292
- # Apply formula modifications
293
- if notes_data and isinstance(notes_data, dict):
294
- for sheet_name, df in notes_data.items():
295
- if isinstance(df, pd.DataFrame) and len(df.columns) >= 2:
296
- df_copy = df.copy()
297
-
298
- # Look for the operands in the dataframe
299
- operand1_col = None
300
- operand2_col = None
301
- total_col = None
302
-
303
- # Find columns containing the operands
304
- for col in df_copy.columns:
305
- col_str = str(col).lower()
306
- if operand1.lower() in col_str:
307
- operand1_col = col
308
- if operand2.lower() in col_str:
309
- operand2_col = col
310
- if 'total' in col_str:
311
- total_col = col
312
-
313
- # If we found the operand columns, create or update total
314
- if operand1_col is not None and operand2_col is not None:
315
- # Calculate the formula: operand1 - operand2
316
- try:
317
- # Convert to numeric, handling any non-numeric values
318
- op1_values = pd.to_numeric(df_copy[operand1_col], errors='coerce')
319
- op2_values = pd.to_numeric(df_copy[operand2_col], errors='coerce')
320
-
321
- # Calculate total = operand1 - operand2
322
- calculated_total = op1_values - op2_values
323
-
324
- # Add or update total column
325
- if total_col is None:
326
- # Find a good position for total column (usually after the operands)
327
- cols = list(df_copy.columns)
328
- max_idx = max(cols.index(operand1_col), cols.index(operand2_col))
329
- cols.insert(max_idx + 1, 'Total')
330
- df_copy['Total'] = calculated_total
331
- df_copy = df_copy[cols]
332
- else:
333
- df_copy[total_col] = calculated_total
334
-
335
- print(f"Applied formula: Total = {operand1} - {operand2}")
336
- print(f"Sample calculation: {{op1_values.iloc[0] if len(op1_values) > 0 else 'N/A'}} - {{op2_values.iloc[0] if len(op2_values) > 0 else 'N/A'}} = {{calculated_total.iloc[0] if len(calculated_total) > 0 else 'N/A'}}")
337
-
338
- except Exception as e:
339
- print(f"Error applying formula: {{e}}")
340
-
341
- notes_data[sheet_name] = df_copy
342
 
343
  return notes_data
344
  '''
@@ -428,13 +454,11 @@ class LLMNotesGenerator(BaseGenerator):
428
  result = run_rlhf_workflow(file_path, "notes-llm")
429
  else:
430
  from agents.langgraph import run_workflow
431
- result = run_workflow(file_path, "notes-llm")
432
 
433
  if result["status"] == "success":
434
- # Apply UDFs to the result if available
435
- if udfs_to_apply:
436
- result = self._apply_udfs_to_result(result, udfs_to_apply, feedback_context)
437
-
438
  return GenerationResult(
439
  success=True,
440
  output_path=result["result"]["output_xlsx_path"],
 
201
  apply_detailed_depreciation = 'depreciation' in feedback_lower and 'asset' in feedback_lower
202
  apply_increase_detail = 'detail' in feedback_lower
203
 
204
+ # Handle different feedback types
205
  if feedback_type == 'formula':
206
  return self._generate_formula_udf(feedback_text, iteration)
207
+ elif feedback_type == 'text':
208
+ return self._generate_text_udf(feedback_text, iteration)
209
+ elif feedback_type == 'suggestion':
210
+ return self._generate_suggestion_udf(feedback_text, iteration)
211
+ else:
212
+ return self._generate_general_udf(feedback_text, feedback_type, iteration)
213
 
214
+ def _generate_text_udf(self, feedback_text: str, iteration: int) -> str:
215
+ """Generate UDF for text feedback"""
216
+ return f'''def apply_user_feedback_v{iteration}(notes_data, feedback_type='text'):
217
  """
218
+ UDF generated from text feedback iteration {iteration}
219
  Original Feedback: {feedback_text}
 
220
  Generated: {datetime.now().isoformat()}
221
  """
 
222
  import re
223
+
224
+ if notes_data and isinstance(notes_data, dict) and 'notes' in notes_data:
225
+ # Extract target note number
226
+ feedback_lower = "{feedback_text}".lower()
227
+ note_match = re.search(r'note\\s*(\\d+)', feedback_lower)
228
+ target_note = note_match.group(1) if note_match else None
229
+
230
+ for note in notes_data['notes']:
231
+ note_num = note.get('metadata', {{}}).get('note_number', '')
232
+
233
+ if not target_note or note_num == target_note:
234
+ # Add text feedback to assumptions or create user_notes field
235
+ if 'assumptions' in note:
236
+ note['assumptions'] += f" [User Note: {feedback_text}]"
237
+ else:
238
+ note['user_notes'] = note.get('user_notes', [])
239
+ note['user_notes'].append(feedback_text)
240
+
241
+ return notes_data
242
+ '''
243
 
244
+ def _generate_suggestion_udf(self, feedback_text: str, iteration: int) -> str:
245
+ """Generate UDF for suggestion feedback"""
246
+ return f'''def apply_user_feedback_v{iteration}(notes_data, feedback_type='suggestion'):
247
+ """
248
+ UDF generated from suggestion feedback iteration {iteration}
249
+ Original Feedback: {feedback_text}
250
+ Generated: {datetime.now().isoformat()}
251
+ """
252
+ import re
253
+ if notes_data and isinstance(notes_data, dict) and 'notes' in notes_data:
254
+ # Extract target note number
255
+ feedback_lower = "{feedback_text}".lower()
256
+ note_match = re.search(r'note\\s*(\\d+)', feedback_lower)
257
+ target_note = note_match.group(1) if note_match else None
258
+
259
+ for note in notes_data['notes']:
260
+ note_num = note.get('metadata', {{}}).get('note_number', '')
261
+
262
+ if not target_note or note_num == target_note:
263
+ # Apply suggestions
264
+ note['user_suggestions'] = note.get('user_suggestions', [])
265
+ note['user_suggestions'].append(feedback_text)
266
+
267
+ # Parse common suggestions
268
+ if 'add' in feedback_lower and 'breakdown' in feedback_lower:
269
+ note['enhanced_breakdown'] = True
270
+ elif 'more detail' in feedback_lower:
271
+ note['detail_level'] = 'enhanced'
272
+
 
 
 
 
273
  return notes_data
274
  '''
275
 
276
+ def _generate_general_udf(self, feedback_text: str, feedback_type: str, iteration: int) -> str:
277
+ """Generate general UDF for other feedback types"""
278
+ return f'''def apply_user_feedback_v{iteration}(notes_data, feedback_type='{feedback_type}'):
279
+ """
280
+ UDF generated from {feedback_type} feedback iteration {iteration}
281
+ Original Feedback: {feedback_text}
282
+ Generated: {datetime.now().isoformat()}
283
+ """
284
+
285
+ if notes_data and isinstance(notes_data, dict) and 'notes' in notes_data:
286
+ for note in notes_data['notes']:
287
+ # Apply general feedback
288
+ note['user_feedback'] = note.get('user_feedback', [])
289
+ note['user_feedback'].append({{
290
+ 'type': '{feedback_type}',
291
+ 'text': '{feedback_text}',
292
+ 'iteration': {iteration}
293
+ }})
294
+
295
+ return notes_data
296
+ '''
297
 
298
  def _generate_formula_udf(self, feedback_text: str, iteration: int) -> str:
299
  """Generate UDF specifically for formula feedback"""
 
325
  """
326
  UDF generated from formula feedback iteration {iteration}
327
  Original Feedback: {feedback_text}
 
328
  Generated: {datetime.now().isoformat()}
329
  """
330
+ import re
331
+
332
+ # Apply formula modifications to JSON structure
333
+ if notes_data and isinstance(notes_data, dict) and 'notes' in notes_data:
334
+ # Extract note number and formula from feedback
335
+ feedback_lower = "{feedback_text}".lower()
336
+ note_match = re.search(r'note\\s*(\\d+)', feedback_lower)
337
+ target_note = note_match.group(1) if note_match else None
338
+
339
+ # Parse formula operators
340
+ operand1, operand2 = "{operand1}", "{operand2}"
341
+
342
+ for note in notes_data['notes']:
343
+ note_num = note.get('metadata', {{}}).get('note_number', '')
344
+
345
+ if not target_note or note_num == target_note:
346
+ if 'structure' in note:
347
+ for item in note['structure']:
348
+ if 'subcategories' in item:
349
+ vals = {{}}
350
+
351
+ for sub in item['subcategories']:
352
+ label = sub.get('label', '').lower()
353
+ if operand1.lower() in label:
354
+ try:
355
+ vals[operand1] = float(sub.get('value', 0))
356
+ except:
357
+ vals[operand1] = 0
358
+ elif operand2.lower() in label:
359
+ try:
360
+ vals[operand2] = float(sub.get('value', 0))
361
+ except:
362
+ vals[operand2] = 0
363
+
364
+ if len(vals) == 2:
365
+ result = vals[operand1] - vals[operand2]
366
+ item['total'] = str(result)
367
+ print(f"Applied formula in note {{note_num}}: {{vals[operand1]}} - {{vals[operand2]}} = {{result}}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
 
369
  return notes_data
370
  '''
 
454
  result = run_rlhf_workflow(file_path, "notes-llm")
455
  else:
456
  from agents.langgraph import run_workflow
457
+ result = run_workflow(file_path, "notes-llm", feedback_context=feedback_context)
458
 
459
  if result["status"] == "success":
460
+ # UDFs are now applied in generate_llm_notes function before Excel conversion
461
+
 
 
462
  return GenerationResult(
463
  success=True,
464
  output_path=result["result"]["output_xlsx_path"],
agents/langgraph.py CHANGED
@@ -23,8 +23,13 @@ def make_workflow(tool_func):
23
  def node(state: FinancialAgentState) -> FinancialAgentState:
24
  state["start_time"] = time.time()
25
  try:
 
 
 
 
 
26
  # Use .invoke() to avoid deprecation warning
27
- result = tool_func.invoke({"file_path": state["file_path"]})
28
  state["result"] = result
29
  state["status"] = "success" if result.get("status") == "success" else "error"
30
  state["error"] = result.get("error", "")
@@ -48,7 +53,7 @@ workflows = {
48
  "notes-llm": make_workflow(generate_llm_notes),
49
  }
50
 
51
- def run_workflow(file_path: str, kind: str) -> Dict[str, Any]:
52
  state = FinancialAgentState(
53
  messages=[HumanMessage(content=f"Run {kind} for {file_path}")],
54
  file_path=file_path,
@@ -58,5 +63,8 @@ def run_workflow(file_path: str, kind: str) -> Dict[str, Any]:
58
  end_time=0,
59
  error="",
60
  )
 
 
 
61
  final = workflows[kind].invoke(state)
62
  return final
 
23
  def node(state: FinancialAgentState) -> FinancialAgentState:
24
  state["start_time"] = time.time()
25
  try:
26
+ # Prepare parameters for tool invocation
27
+ tool_params = {"file_path": state["file_path"]}
28
+ # Add feedback_context if available
29
+ if "feedback_context" in state:
30
+ tool_params["feedback_context"] = state["feedback_context"]
31
  # Use .invoke() to avoid deprecation warning
32
+ result = tool_func.invoke(tool_params)
33
  state["result"] = result
34
  state["status"] = "success" if result.get("status") == "success" else "error"
35
  state["error"] = result.get("error", "")
 
53
  "notes-llm": make_workflow(generate_llm_notes),
54
  }
55
 
56
+ def run_workflow(file_path: str, kind: str, **kwargs) -> Dict[str, Any]:
57
  state = FinancialAgentState(
58
  messages=[HumanMessage(content=f"Run {kind} for {file_path}")],
59
  file_path=file_path,
 
63
  end_time=0,
64
  error="",
65
  )
66
+ # Add feedback_context if provided
67
+ if "feedback_context" in kwargs:
68
+ state["feedback_context"] = kwargs["feedback_context"]
69
  final = workflows[kind].invoke(state)
70
  return final
agents/simple_tools.py CHANGED
@@ -9,6 +9,7 @@ import json
9
  import shutil
10
  import time
11
  import uuid
 
12
  from typing import Dict, Any
13
  import logging
14
 
@@ -371,7 +372,7 @@ def generate_cash_flow_statement(file_path: str) -> Dict[str, Any]:
371
  }
372
 
373
  @tool
374
- def generate_llm_notes(file_path: str, note_numbers: str = "") -> Dict[str, Any]:
375
  """
376
  Generate notes using LLM-based approach (FlexibleFinancialNoteGenerator)
377
  Args:
@@ -430,7 +431,12 @@ def generate_llm_notes(file_path: str, note_numbers: str = "") -> Dict[str, Any]
430
  # Step 3: Convert to Excel
431
  logger.info("Step 3: Converting to Excel format")
432
  input_json = "data/generated_notes/notes.json"
433
- output_excel = "data/generated_notes_excel/notes.xlsx"
 
 
 
 
 
434
 
435
  # Check if the JSON file was created and has content
436
  if not os.path.exists(input_json):
@@ -442,6 +448,42 @@ def generate_llm_notes(file_path: str, note_numbers: str = "") -> Dict[str, Any]
442
  "execution_time": execution_time
443
  }
444
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
  # Check if JSON file has content
446
  try:
447
  with open(input_json, 'r', encoding='utf-8') as f:
 
9
  import shutil
10
  import time
11
  import uuid
12
+ from datetime import datetime
13
  from typing import Dict, Any
14
  import logging
15
 
 
372
  }
373
 
374
  @tool
375
+ def generate_llm_notes(file_path: str, note_numbers: str = "", **kwargs) -> Dict[str, Any]:
376
  """
377
  Generate notes using LLM-based approach (FlexibleFinancialNoteGenerator)
378
  Args:
 
431
  # Step 3: Convert to Excel
432
  logger.info("Step 3: Converting to Excel format")
433
  input_json = "data/generated_notes/notes.json"
434
+
435
+ # Create unique output path in llm_generated folder
436
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
437
+ output_folder = "data/notes_llm_generated"
438
+ os.makedirs(output_folder, exist_ok=True)
439
+ output_excel = f"{output_folder}/new_{timestamp}_{execution_id}.xlsx"
440
 
441
  # Check if the JSON file was created and has content
442
  if not os.path.exists(input_json):
 
448
  "execution_time": execution_time
449
  }
450
 
451
+ # Apply UDFs if provided in kwargs
452
+ feedback_context = kwargs.get('feedback_context', {})
453
+ udfs_to_apply = feedback_context.get('udfs', [])
454
+ if udfs_to_apply:
455
+ try:
456
+ # Load JSON data
457
+ with open(input_json, 'r', encoding='utf-8') as f:
458
+ notes_data = json.load(f)
459
+
460
+ # Apply each UDF
461
+ for udf_code in udfs_to_apply:
462
+ try:
463
+ local_vars = {}
464
+ exec(udf_code, {"datetime": datetime}, local_vars)
465
+
466
+ # Find the UDF function
467
+ udf_func = None
468
+ for var_name, var_value in local_vars.items():
469
+ if callable(var_value) and var_name.startswith('apply_user_feedback'):
470
+ udf_func = var_value
471
+ break
472
+
473
+ if udf_func:
474
+ notes_data = udf_func(notes_data, feedback_context.get('feedback_type', 'general'))
475
+ logger.info(f"Applied UDF successfully")
476
+ except Exception as e:
477
+ logger.warning(f"Failed to apply UDF: {e}")
478
+ continue
479
+
480
+ # Save modified JSON back
481
+ with open(input_json, 'w', encoding='utf-8') as f:
482
+ json.dump(notes_data, f, ensure_ascii=False, indent=2)
483
+
484
+ except Exception as e:
485
+ logger.error(f"Error applying UDFs to JSON: {e}")
486
+
487
  # Check if JSON file has content
488
  try:
489
  with open(input_json, 'r', encoding='utf-8') as f:
app.py CHANGED
@@ -216,9 +216,15 @@ async def generate_with_feedback(
216
  pipeline = create_notes_pipeline(use_rlhf=False)
217
 
218
  # Prepare feedback context for the generator
 
 
 
 
 
 
219
  feedback_context = {
220
  'session_id': session_id,
221
- 'udfs': session.archived_udfs, # Pass all archived UDFs
222
  'feedback_history': [
223
  {
224
  'text': f.feedback_text,
 
216
  pipeline = create_notes_pipeline(use_rlhf=False)
217
 
218
  # Prepare feedback context for the generator
219
+ udfs_to_apply = []
220
+ if session.final_udf:
221
+ udfs_to_apply.append(session.final_udf)
222
+ elif session.archived_udfs:
223
+ udfs_to_apply.extend(session.archived_udfs)
224
+
225
  feedback_context = {
226
  'session_id': session_id,
227
+ 'udfs': udfs_to_apply, # Pass final UDF if available, otherwise archived UDFs
228
  'feedback_history': [
229
  {
230
  'text': f.feedback_text,
notes/llm_notes_generator.py CHANGED
@@ -23,6 +23,7 @@ from typing import Dict, List, Any, Optional, Tuple
23
  import pandas as pd
24
  from pydantic import BaseModel, ValidationError
25
  from pydantic_settings import BaseSettings
 
26
  from utils.utils import convert_note_json_to_lakhs
27
 
28
  # Load environment variables
@@ -33,9 +34,8 @@ logging.basicConfig(level=logging.INFO)
33
  logger = logging.getLogger(__name__)
34
 
35
  class Settings(BaseSettings):
36
- """Application settings loaded from environment variables or .env file."""
37
- mistral_api_key: str = os.getenv('MISTRAL_API_KEY', '')
38
- api_url: str = "https://api.mistral.ai/v1/chat/completions"
39
  output_dir: str = "data/generated_notes"
40
  trial_balance_json: str = "data/output1/parsed_trial_balance.json"
41
 
@@ -61,20 +61,22 @@ class GeneratedNote(BaseModel):
61
 
62
  class FlexibleFinancialNoteGenerator:
63
  def __init__(self):
64
- self.mistral_api_key = settings.mistral_api_key
65
- if not self.mistral_api_key:
66
- logger.error("MISTRAL_API_KEY not found in .env file")
67
- raise ValueError("MISTRAL_API_KEY not found in .env file")
68
  self.api_url = settings.api_url
69
  self.headers = {
70
- "Authorization": f"Bearer {self.mistral_api_key}",
71
- "Content-Type": "application/json"
 
 
72
  }
73
  self.note_templates = self.load_note_templates()
74
  self.account_patterns = self._init_account_patterns()
75
  self.recommended_models = [
76
- "mistral-large-latest",
77
- "mistral-medium-latest"
78
  ]
79
 
80
  def _init_account_patterns(self) -> Dict[str, Dict[str, Any]]:
@@ -338,8 +340,8 @@ class FlexibleFinancialNoteGenerator:
338
 
339
  return prompt
340
 
341
- def call_mistral_api(self, prompt: str) -> Optional[str]:
342
- """Make API call to Mistral with model fallback"""
343
  for model in self.recommended_models:
344
  logger.info(f"Trying model: {model}")
345
  payload = {
@@ -357,7 +359,7 @@ class FlexibleFinancialNoteGenerator:
357
  self.api_url,
358
  headers=self.headers,
359
  json=payload,
360
- timeout=30
361
  )
362
  response.raise_for_status()
363
  result = response.json()
@@ -448,7 +450,7 @@ class FlexibleFinancialNoteGenerator:
448
  logger.error("Failed to build prompt")
449
  return False
450
 
451
- response = self.call_mistral_api(prompt)
452
  if not response:
453
  logger.error("Failed to get API response")
454
  return False
@@ -473,7 +475,7 @@ class FlexibleFinancialNoteGenerator:
473
  if not prompt:
474
  results[note_number] = False
475
  continue
476
- response = self.call_mistral_api(prompt)
477
  if not response:
478
  results[note_number] = False
479
  continue
 
23
  import pandas as pd
24
  from pydantic import BaseModel, ValidationError
25
  from pydantic_settings import BaseSettings
26
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
27
  from utils.utils import convert_note_json_to_lakhs
28
 
29
  # Load environment variables
 
34
  logger = logging.getLogger(__name__)
35
 
36
  class Settings(BaseSettings):
37
+ openrouter_api_key: str = os.getenv('OPENROUTER_API_KEY', '')
38
+ api_url: str = "https://openrouter.ai/api/v1/chat/completions"
 
39
  output_dir: str = "data/generated_notes"
40
  trial_balance_json: str = "data/output1/parsed_trial_balance.json"
41
 
 
61
 
62
  class FlexibleFinancialNoteGenerator:
63
  def __init__(self):
64
+ self.openrouter_api_key = settings.openrouter_api_key
65
+ if not self.openrouter_api_key:
66
+ logger.error("OPENROUTER_API_KEY not found in .env file")
67
+ raise ValueError("OPENROUTER_API_KEY not found in .env file")
68
  self.api_url = settings.api_url
69
  self.headers = {
70
+ "Authorization": f"Bearer {self.openrouter_api_key}",
71
+ "Content-Type": "application/json",
72
+ "HTTP-Referer": "https://localhost:3000",
73
+ "X-Title": "Financial Note Generator"
74
  }
75
  self.note_templates = self.load_note_templates()
76
  self.account_patterns = self._init_account_patterns()
77
  self.recommended_models = [
78
+ "mistralai/mixtral-8x7b-instruct",
79
+ "mistralai/mistral-7b-instruct-v0.2"
80
  ]
81
 
82
  def _init_account_patterns(self) -> Dict[str, Dict[str, Any]]:
 
340
 
341
  return prompt
342
 
343
+ def call_openrouter_api(self, prompt: str) -> Optional[str]:
344
+ """Make API call to OpenRouter with model fallback"""
345
  for model in self.recommended_models:
346
  logger.info(f"Trying model: {model}")
347
  payload = {
 
359
  self.api_url,
360
  headers=self.headers,
361
  json=payload,
362
+ timeout=30 # <-- Add timeout here!
363
  )
364
  response.raise_for_status()
365
  result = response.json()
 
450
  logger.error("Failed to build prompt")
451
  return False
452
 
453
+ response = self.call_openrouter_api(prompt)
454
  if not response:
455
  logger.error("Failed to get API response")
456
  return False
 
475
  if not prompt:
476
  results[note_number] = False
477
  continue
478
+ response = self.call_openrouter_api(prompt)
479
  if not response:
480
  results[note_number] = False
481
  continue