Spaces:
Sleeping
Sleeping
File size: 18,097 Bytes
7e453aa 41cb3f5 7e453aa ddaebec c79824c 7e453aa 6611563 850182e 41cb3f5 c172f37 6b07055 f39814a ddaebec a21082a f39814a 850182e ddaebec f39814a c172f37 a21082a 6b07055 a21082a 6b07055 a21082a 6b07055 a21082a 850182e a21082a c172f37 a21082a 41cb3f5 c79824c 7e453aa 8a2f766 7e453aa 5694766 532f273 7e453aa f39814a 7e453aa c79824c 2ac8811 5694766 2ac8811 850182e 2ac8811 5694766 2ac8811 5694766 532f273 5694766 2ac8811 24ccd4e a9ec4f6 850182e ddaebec 850182e 5694766 850182e ddaebec 850182e 5694766 850182e 29ee329 6611563 5694766 6611563 850182e 5694766 850182e dea72cd 6611563 dea72cd a21082a dea72cd c172f37 dea72cd c172f37 dea72cd a21082a c172f37 dea72cd a21082a dea72cd 16c82fe 532f273 dea72cd c172f37 16c82fe c172f37 16c82fe c172f37 dea72cd c172f37 dea72cd 532f273 dea72cd a21082a dea72cd c172f37 dea72cd c172f37 dea72cd a21082a c172f37 dea72cd a21082a f39814a 5694766 |
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 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 |
from fastapi import FastAPI, APIRouter, UploadFile, File, HTTPException, Query, Form
from fastapi.responses import FileResponse
from typing import Optional
from contextlib import asynccontextmanager
import os
import shutil
import logging
import json
from agents.simple_tools import generate_notes_full_pipeline_from_path
from agents.generator_validator import create_notes_pipeline, InteractiveFeedbackManager
from agents.langgraph import run_workflow
from agents.rlhf_workflows import run_rlhf_workflow
from agents.rlhf_routes import rlhf_router
from fastapi.middleware.cors import CORSMiddleware
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("financial_notes_api")
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
logger.info("Financial Notes Generator API has started.")
yield
# Shutdown
logger.info("Financial Notes Generator API is shutting down.")
# Initialize FastAPI app first
app = FastAPI(
title="Financial Notes Generator API",
description="API for generating financial notes, balance sheets, cash flow statements, and P&L reports with RLHF capabilities and Interactive Feedback.",
version="1.0.0",
lifespan=lifespan
)
# Add CORS middleware immediately after app initialization
# Using "*" for debugging - restrict this in production
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Temporarily allow all origins for debugging
allow_credentials=True,
allow_methods=["*"], # Allow all methods (GET, POST, OPTIONS, etc.)
allow_headers=["*"], # Allow all headers
expose_headers=["*"], # Expose all custom headers to frontend
)
# Initialize feedback manager
feedback_manager = InteractiveFeedbackManager()
# Include RLHF router
app.include_router(rlhf_router)
# Initialize router for main endpoints
router = APIRouter()
@router.get("/")
async def root():
"""
Root endpoint for the Financial Notes Generator API.
Returns basic API information.
"""
return {
"message": "Welcome to Financial Notes Generator API",
"version": "1.0.0",
"description": "API for generating financial notes, balance sheets, cash flow statements, and P&L reports",
"endpoints": {
"notes": "POST /notes - Generate financial notes from trial balance",
"notes-llm": "POST /notes-llm - Generate LLM-based notes with interactive feedback",
"bs": "POST /bs - Generate balance sheet",
"pnl": "POST /pnl - Generate P&L statement",
"cf": "POST /cf - Generate cash flow statement",
"docs": "/docs - API documentation"
}
}
@router.post("/notes-llm")
async def notes_llm_route(
file: UploadFile = File(...),
use_rlhf: bool = Query(False),
user_api_key: Optional[str] = Form(None)
):
if not user_api_key or user_api_key.strip() == "":
raise HTTPException(
status_code=400,
detail="Missing required parameter: 'user_api_key'. Please provide your OpenRouter API key as a form parameter (not in JSON body)."
)
file_path = f"data/input/{file.filename}"
os.makedirs("data/input", exist_ok=True)
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
try:
pipeline = create_notes_pipeline(use_rlhf=use_rlhf, user_api_key=user_api_key)
generation_result, validation_result = pipeline.process(file_path)
summary = pipeline.get_processing_summary()
logger.info(f"LLM Notes Pipeline Summary: {summary}")
if generation_result.success and validation_result.is_valid:
session_id = feedback_manager.create_session(file_path)
response = FileResponse(
generation_result.output_path,
filename=os.path.basename(generation_result.output_path)
)
response.headers["X-Generation-Method"] = "llm"
response.headers["X-Validation-Score"] = str(validation_result.score)
response.headers["X-Attempts-Made"] = str(generation_result.metadata.get("attempt", 1))
response.headers["X-Execution-ID"] = generation_result.metadata.get("execution_id", "")
response.headers["X-Session-ID"] = session_id
response.headers["X-Interactive-Enabled"] = "true"
if use_rlhf and "rlhf_metadata" in generation_result.metadata:
rlhf_data = generation_result.metadata["rlhf_metadata"]
response.headers["X-RLHF-Statement-ID"] = str(rlhf_data.get("statement_id", ""))
response.headers["X-RLHF-Quality-Score"] = str(rlhf_data.get("predicted_quality", ""))
response.headers["X-RLHF-Confidence"] = str(rlhf_data.get("confidence_score", ""))
if validation_result.feedback:
response.headers["X-Validation-Feedback"] = json.dumps(validation_result.feedback)
return response
else:
error_detail = {
"generation_error": generation_result.error,
"validation_feedback": validation_result.feedback,
"validation_score": validation_result.score,
"attempts_made": generation_result.metadata.get("attempt", 1),
"processing_summary": summary
}
raise HTTPException(status_code=500, detail=json.dumps(error_detail))
except ValueError as ve:
logger.error(f"API key error: {ve}")
if "API key is required" in str(ve):
raise HTTPException(status_code=400, detail="Missing OpenRouter API key. Please provide your API key via the 'user_api_key' form parameter.")
raise HTTPException(status_code=400, detail=str(ve))
except Exception as e:
logger.error(f"LLM Notes pipeline failed: {e}")
raise HTTPException(status_code=500, detail=f"Pipeline processing failed: {str(e)}")
@router.post("/notes-llm/feedback")
async def submit_feedback(
session_id: str = Form(...),
feedback_text: str = Form(...),
feedback_type: str = Form(..., pattern="^(text|numeric|formula|suggestion)$")
):
try:
udf_version = feedback_manager.add_feedback(session_id, feedback_text, feedback_type)
if udf_version is None:
raise HTTPException(status_code=404, detail="Session not found")
return {
"status": "success",
"session_id": session_id,
"udf_version": udf_version,
"iteration": feedback_manager.get_session(session_id).current_iteration,
"message": "Feedback submitted and UDF generated successfully"
}
except Exception as e:
logger.error(f"Feedback submission failed: {e}")
raise HTTPException(status_code=500, detail=f"Feedback submission failed: {str(e)}")
@router.post("/notes-llm/approve")
async def approve_session(session_id: str = Form(...)):
try:
success = feedback_manager.approve_session(session_id)
if not success:
raise HTTPException(status_code=404, detail="Session not found")
session = feedback_manager.get_session(session_id)
return {
"status": "approved",
"session_id": session_id,
"final_udf": session.final_udf,
"total_iterations": session.current_iteration,
"archived_udfs_count": len(session.archived_udfs),
"message": "Session approved and final UDF set"
}
except Exception as e:
logger.error(f"Session approval failed: {e}")
raise HTTPException(status_code=500, detail=f"Session approval failed: {str(e)}")
@router.get("/notes-llm/session/{session_id}")
async def get_session_info(session_id: str):
try:
session = feedback_manager.get_session(session_id)
if not session:
raise HTTPException(status_code=404, detail="Session not found")
return {
"session_id": session.session_id,
"status": session.status,
"current_iteration": session.current_iteration,
"total_feedbacks": len(session.feedback_history),
"archived_udfs_count": len(session.archived_udfs),
"final_udf": session.final_udf,
"created_at": session.created_at.isoformat(),
"last_updated": session.last_updated.isoformat(),
"feedback_history": [
{
"iteration": f.iteration_number,
"feedback_type": f.feedback_type,
"feedback_text": f.feedback_text,
"udf_version": f.udf_version,
"timestamp": f.timestamp.isoformat(),
"changes_description": f.changes_description
} for f in session.feedback_history
]
}
except Exception as e:
logger.error(f"Session info retrieval failed: {e}")
raise HTTPException(status_code=500, detail=f"Session info retrieval failed: {str(e)}")
@router.post("/notes-llm/generate")
async def generate_with_feedback(
session_id: str = Form(...),
file: UploadFile = File(...),
user_api_key: Optional[str] = Form(None)
):
if not user_api_key or user_api_key.strip() == "":
raise HTTPException(
status_code=400,
detail="Missing required parameter: 'user_api_key'. Please provide your OpenRouter API key as a form parameter (not in JSON body)."
)
try:
session = feedback_manager.get_session(session_id)
if not session:
raise HTTPException(status_code=404, detail="Session not found")
if session.status != 'active':
raise HTTPException(status_code=400, detail=f"Session is {session.status}")
file_path = f"data/input/{file.filename}"
os.makedirs("data/input", exist_ok=True)
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
pipeline = create_notes_pipeline(use_rlhf=False, user_api_key=user_api_key)
udfs_to_apply = []
if session.final_udf:
udfs_to_apply.append(session.final_udf)
elif session.archived_udfs:
udfs_to_apply.extend(session.archived_udfs)
feedback_context = {
'session_id': session_id,
'udfs': udfs_to_apply,
'feedback_history': [
{
'text': f.feedback_text,
'type': f.feedback_type,
'iteration': f.iteration_number
} for f in session.feedback_history
],
'current_iteration': session.current_iteration
}
generation_result, validation_result = pipeline.process(file_path, feedback_context=feedback_context)
if generation_result.success and validation_result.is_valid:
response = FileResponse(
generation_result.output_path,
filename=os.path.basename(generation_result.output_path)
)
response.headers["X-Session-ID"] = session_id
response.headers["X-Iteration"] = str(session.current_iteration)
response.headers["X-Feedbacks-Applied"] = str(len(session.feedback_history))
response.headers["X-UDFs-Archived"] = str(len(session.archived_udfs))
response.headers["X-Generation-Method"] = "llm_with_feedback"
response.headers["X-Validation-Score"] = str(validation_result.score)
response.headers["X-Execution-ID"] = generation_result.metadata.get("execution_id", "")
return response
else:
error_detail = {
"generation_error": generation_result.error,
"validation_feedback": validation_result.feedback,
"validation_score": validation_result.score,
"session_id": session_id,
"current_iteration": session.current_iteration
}
raise HTTPException(status_code=500, detail=json.dumps(error_detail))
except HTTPException:
raise
except ValueError as ve:
logger.error(f"API key error: {ve}")
raise HTTPException(status_code=400, detail=str(ve))
except Exception as e:
logger.error(f"Feedback-based generation failed: {e}")
raise HTTPException(status_code=500, detail=f"Generation failed: {str(e)}")
@router.post("/notes")
async def notes_route(file: UploadFile = File(...)):
try:
file_path = f"data/input/{file.filename}"
os.makedirs("data/input", exist_ok=True)
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
result = generate_notes_full_pipeline_from_path(file_path)
if result["status"] == "success":
output_path = result["output_xlsx_path"]
return FileResponse(output_path, filename=os.path.basename(output_path))
raise HTTPException(status_code=500, detail=result.get("error", "Notes generation failed"))
except Exception as e:
logger.error(f"Error in notes generation: {e}")
raise HTTPException(status_code=500, detail=f"Error generating notes: {str(e)}")
@router.post("/pnl")
async def pnl_route(file: UploadFile = File(...), use_rlhf: bool = Query(False)):
file_path = f"data/input/{file.filename}"
os.makedirs("data/input", exist_ok=True)
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
if use_rlhf:
result = run_rlhf_workflow(file_path, "pnl")
else:
result = run_workflow(file_path, "pnl")
if result["status"] == "success":
response = FileResponse(
result["result"].get("output_path", "data/pnl_statement.xlsx"),
filename=os.path.basename(result["result"].get("output_path", "data/pnl_statement.xlsx"))
)
if "rlhf_metadata" in result.get("result", {}):
rlhf_data = result["result"]["rlhf_metadata"]
response.headers["X-RLHF-Statement-ID"] = str(rlhf_data.get("statement_id", ""))
response.headers["X-RLHF-Quality-Score"] = str(rlhf_data.get("predicted_quality", ""))
response.headers["X-RLHF-Confidence"] = str(rlhf_data.get("confidence_score", ""))
return response
raise HTTPException(status_code=500, detail=result["error"])
@router.post("/bs")
async def bs_route(file: UploadFile = File(...), use_rlhf: bool = Query(False), user_api_key: Optional[str] = Form(None)):
if not user_api_key or user_api_key.strip() == "":
raise HTTPException(
status_code=400,
detail="Missing required parameter: 'user_api_key'. Please provide your OpenRouter API key as a form parameter (not in JSON body)."
)
file_path = f"data/input/{file.filename}"
os.makedirs("data/input", exist_ok=True)
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
if use_rlhf:
result = run_rlhf_workflow(file_path, "bs", user_api_key=user_api_key)
else:
result = run_workflow(file_path, "bs", user_api_key=user_api_key)
if result["status"] == "success":
output_file = result["result"].get("output_path")
if not output_file or not os.path.isfile(output_file):
output_dir = "data/output/"
xlsx_files = [f for f in os.listdir(output_dir) if f.endswith('.xlsx') and os.path.isfile(os.path.join(output_dir, f))]
if xlsx_files:
output_file = os.path.join(output_dir, xlsx_files[0])
else:
raise HTTPException(status_code=500, detail="No balance sheet Excel file produced")
response = FileResponse(output_file, filename=os.path.basename(output_file))
if "rlhf_metadata" in result.get("result", {}):
rlhf_data = result["result"]["rlhf_metadata"]
response.headers["X-RLHF-Statement-ID"] = str(rlhf_data.get("statement_id", ""))
response.headers["X-RLHF-Quality-Score"] = str(rlhf_data.get("predicted_quality", ""))
response.headers["X-RLHF-Confidence"] = str(rlhf_data.get("confidence_score", ""))
return response
else:
error_msg = result.get("error", "Unknown error")
# Check if error is about missing API key
if "Missing OpenRouter API key" in error_msg:
raise HTTPException(
status_code=400,
detail="Missing OpenRouter API key. Please provide your API key via the 'user_api_key' form parameter."
)
raise HTTPException(status_code=500, detail=error_msg)
@router.post("/cf")
async def cf_route(file: UploadFile = File(...), use_rlhf: bool = Query(False)):
file_path = f"data/input/{file.filename}"
os.makedirs("data/input", exist_ok=True)
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
if use_rlhf:
result = run_rlhf_workflow(file_path, "cf")
else:
result = run_workflow(file_path, "cf")
if result["status"] == "success":
response = FileResponse(
result["result"].get("output_path", "data/cash_flow_statements.xlsx"),
filename=os.path.basename(result["result"].get("output_path", "data/cash_flow_statements.xlsx"))
)
if "rlhf_metadata" in result.get("result", {}):
rlhf_data = result["result"]["rlhf_metadata"]
response.headers["X-RLHF-Statement-ID"] = str(rlhf_data.get("statement_id", ""))
response.headers["X-RLHF-Quality-Score"] = str(rlhf_data.get("predicted_quality", ""))
response.headers["X-RLHF-Confidence"] = str(rlhf_data.get("confidence_score", ""))
return response
raise HTTPException(status_code=500, detail=result["error"])
# Include router after all route definitions
app.include_router(router)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7860) |