Spaces:
Running
Running
| from datetime import datetime, timedelta | |
| import json | |
| import logging | |
| from typing import Any, Dict, List, Optional | |
| from accounting.ledger import EventSourcedLedger | |
| from accounting.models import Account, AccountType, EntryType, JournalEntry, Transaction | |
| from sqlalchemy import func | |
| from sqlalchemy.orm import Session | |
| from integrations.ai_enhanced_service import ( | |
| AIModelType, | |
| AIRequest, | |
| AIServiceType, | |
| AITaskType, | |
| ai_enhanced_service, | |
| ) | |
| logger = logging.getLogger(__name__) | |
| class AccountingAssistant: | |
| """ | |
| Assistant for natural language accounting queries and commands. | |
| """ | |
| def __init__(self, db: Session): | |
| self.db = db | |
| self.ledger = EventSourcedLedger(db) | |
| async def process_query(self, workspace_id: str, query: str) -> Dict[str, Any]: | |
| """Process a natural language accounting query""" | |
| # 1. Use AI to understand intent and extract parameters | |
| ai_request = AIRequest( | |
| request_id=f"finance_query_{int(datetime.utcnow().timestamp())}", | |
| task_type=AITaskType.NATURAL_LANGUAGE_COMMANDS, | |
| model_type=AIModelType.GPT_4, | |
| service_type=AIServiceType.OPENAI, | |
| input_data={ | |
| "text": query, | |
| "instruction": ( | |
| "Interpret the accounting query. Is the user asking for a balance, runway, burn rate, " | |
| "or wanting to record a transaction? Return JSON with 'intent', 'params' (dict), and 'reasoning'." | |
| ) | |
| } | |
| ) | |
| try: | |
| ai_response = await ai_enhanced_service.process_ai_request(ai_request) | |
| # For brevity in MVP, we handle some intents directly or via AI result | |
| result = ai_response.output_data | |
| if isinstance(result, str): | |
| try: | |
| result = json.loads(result) | |
| except json.JSONDecodeError as e: | |
| logger.debug(f"Failed to parse AI response as JSON: {e}") | |
| intent = result.get("intent", "unknown") | |
| params = result.get("params", {}) | |
| if intent == "get_balance": | |
| return self._handle_get_balance(workspace_id, params) | |
| elif intent == "get_runway": | |
| return self._handle_get_runway(workspace_id) | |
| elif intent == "check_overdue": | |
| return {"intent": "check_overdue"} # Handled by orchestrator | |
| elif intent == "get_aging": | |
| return {"intent": "get_aging"} # Handled by orchestrator | |
| elif intent == "check_close_readiness": | |
| return {"intent": "check_close_readiness", "params": params} | |
| elif intent == "get_tax_estimate": | |
| return {"intent": "get_tax_estimate"} | |
| elif intent == "get_cash_forecast": | |
| return {"intent": "get_cash_forecast"} | |
| elif intent == "run_scenario": | |
| return {"intent": "run_scenario", "params": params} | |
| elif intent == "get_intercompany_report": | |
| return {"intent": "get_intercompany_report"} | |
| elif intent == "record_transaction": | |
| return await self._handle_record_transaction(workspace_id, query, params) | |
| return { | |
| "answer": "I'm not sure how to help with that financial query yet. I can check balances, runway, or record simple transactions.", | |
| "intent": intent | |
| } | |
| except Exception as e: | |
| logger.error(f"Accounting assistant error: {e}") | |
| return {"answer": f"Sorry, I encountered an error: {str(e)}"} | |
| def _handle_get_balance(self, workspace_id: str, params: Dict) -> Dict[str, Any]: | |
| account_name = params.get("account_name", "Cash") | |
| account = self.db.query(Account).filter( | |
| Account.workspace_id == workspace_id, | |
| Account.name.ilike(f"%{account_name}%") | |
| ).first() | |
| if not account: | |
| return {"answer": f"I couldn't find an account named '{account_name}'."} | |
| balance = self.ledger.get_account_balance(account.id) | |
| return { | |
| "answer": f"The current balance of {account.name} is ${balance:,.2f}.", | |
| "data": {"account": account.name, "balance": balance} | |
| } | |
| def _handle_get_runway(self, workspace_id: str) -> Dict[str, Any]: | |
| # Simple runway calculation: Cash / Avg monthly burn | |
| cash_account = self.db.query(Account).filter( | |
| Account.workspace_id == workspace_id, | |
| Account.code == "1000" | |
| ).first() | |
| if not cash_account: | |
| return {"answer": "I need a cash account to calculate runway."} | |
| cash_balance = self.ledger.get_account_balance(cash_account.id) | |
| # Calculate monthly burn from actual expense transactions (last 30 days) | |
| thirty_days_ago = datetime.utcnow() - timedelta(days=30) | |
| monthly_burn = self.db.query(JournalEntry).join(Transaction).filter( | |
| Transaction.workspace_id == workspace_id, | |
| Transaction.transaction_date >= thirty_days_ago, | |
| JournalEntry.type == EntryType.DEBIT | |
| ).join(Account).filter( | |
| Account.type == AccountType.EXPENSE | |
| ).with_entities( | |
| func.sum(JournalEntry.amount) | |
| ).scalar() or 0.0 | |
| if monthly_burn <= 0: | |
| return {"answer": "Your burn rate is 0 or positive cash flow, so your runway is infinite!"} | |
| runway_months = cash_balance / monthly_burn | |
| return { | |
| "answer": f"Based on your current cash balance of ${cash_balance:,.2f} and a burn rate of ${monthly_burn:,.2f}/mo, your runway is approximately {runway_months:.1f} months.", | |
| "data": {"cash": cash_balance, "burn": monthly_burn, "runway": runway_months} | |
| } | |
| async def _handle_record_transaction(self, workspace_id: str, query: str, params: Dict) -> Dict[str, Any]: | |
| # This would use the TransactionIngestor or DoubleEntryEngine directly | |
| # For MVP, we'll just acknowledge the intent | |
| return { | |
| "answer": "I've understood you want to record a transaction. (Integration with ledger coming in Phase 2!)", | |
| "intent": "record_transaction", | |
| "extracted_params": params | |
| } | |