from __future__ import annotations import hashlib import json from datetime import UTC, date, datetime from decimal import Decimal from pathlib import Path from sqlalchemy import inspect, select, text from app.core.agent_enums import ( AgentAssetContentType, AgentAssetDomain, AgentAssetStatus, AgentAssetType, AgentName, AgentPermissionLevel, AgentReviewStatus, AgentRunSource, AgentRunStatus, AgentToolType, ) from app.models.agent_asset import AgentAsset, AgentAssetReview, AgentAssetVersion from app.models.agent_run import AgentRun, AgentToolCall, SemanticParseLog from app.models.audit_log import AuditLog from app.models.financial_record import ( AccountsPayableRecord, AccountsReceivableRecord, ExpenseClaim, ExpenseClaimItem, ) from app.services.agent_asset_rule_library import AgentAssetRuleLibraryManager from app.services.agent_asset_spreadsheet import ( AgentAssetSpreadsheetManager, COMPANY_COMMUNICATION_EXPENSE_RULE_CODE, COMPANY_COMMUNICATION_EXPENSE_RULE_FILENAME, COMPANY_TRAVEL_EXPENSE_RULE_CODE, COMPANY_TRAVEL_EXPENSE_RULE_FILENAME, FINANCE_RULES_LIBRARY, RISK_RULES_LIBRARY, ) from app.services.expense_rule_runtime import ( build_scene_submission_standard_markdown, build_travel_risk_control_standard_markdown, ) from app.services.agent_foundation_constants import ( ATTACHMENT_RULE_ASSET_CODE, ATTACHMENT_RULE_RUNTIME_CONFIG, COMPANY_COMMUNICATION_RULE_SCENARIO_JSON, COMPANY_COMMUNICATION_RULE_VERSION, COMPANY_TRAVEL_RULE_SCENARIO_JSON, COMPANY_TRAVEL_RULE_VERSION, DEMO_EXPENSE_CLAIM_SIGNATURES, DEMO_PAYABLE_SIGNATURES, DEMO_RECEIVABLE_SIGNATURES, LEGACY_RULE_CODES, PLATFORM_DESTINATION_LOCATION_RULE_FILENAME, ) from app.core.logging import get_logger logger = get_logger("app.services.agent_foundation") class AgentFoundationAssetHelperMixin: def _create_seed_asset( self, *, asset_type: str, code: str, name: str, description: str, domain: str, scenario_json: list[str], owner: str, reviewer: str, status: str, current_version: str, config_json: dict[str, object], ) -> AgentAsset: asset = AgentAsset( asset_type=asset_type, code=code, name=name, description=description, domain=domain, scenario_json=scenario_json, owner=owner, reviewer=reviewer, status=status, current_version=current_version, published_version=current_version if status == AgentAssetStatus.ACTIVE.value else None, working_version=current_version, config_json=config_json, ) self.db.add(asset) self.db.flush() return asset def _ensure_asset_version( self, asset: AgentAsset, *, version: str, content: str, content_type: str, change_note: str, created_by: str, ) -> None: existing = self.db.scalar( select(AgentAssetVersion).where( AgentAssetVersion.asset_id == asset.id, AgentAssetVersion.version == version, ) ) if existing is not None: return self.db.add( AgentAssetVersion( asset_id=asset.id, version=version, content=content, content_type=content_type, change_note=change_note, created_by=created_by, ) ) def _ensure_asset_review( self, asset: AgentAsset, *, version: str, reviewer: str, review_status: str, review_note: str, reviewed_at: datetime | None, ) -> None: existing = self.db.scalar( select(AgentAssetReview).where( AgentAssetReview.asset_id == asset.id, AgentAssetReview.version == version, AgentAssetReview.review_status == review_status, ) ) if existing is not None: return self.db.add( AgentAssetReview( asset_id=asset.id, version=version, reviewer=reviewer, review_status=review_status, review_note=review_note, reviewed_at=reviewed_at, ) ) def _remove_legacy_rule_assets(self) -> None: assets = list( self.db.scalars( select(AgentAsset).where(AgentAsset.code.in_(LEGACY_RULE_CODES)) ).all() ) for asset in assets: self.db.delete(asset) obsolete_logs = list( self.db.scalars( select(AuditLog).where(AuditLog.resource_id.in_(LEGACY_RULE_CODES)) ).all() ) for log in obsolete_logs: self.db.delete(log) def _ensure_agent_asset_schema(self) -> None: bind = self.db.get_bind() inspector = inspect(bind) if "agent_assets" not in inspector.get_table_names(): return column_names = {column["name"] for column in inspector.get_columns("agent_assets")} migration_statements: list[str] = [] if "published_version" not in column_names: migration_statements.append("ALTER TABLE agent_assets ADD COLUMN published_version VARCHAR(30)") if "working_version" not in column_names: migration_statements.append("ALTER TABLE agent_assets ADD COLUMN working_version VARCHAR(30)") for statement in migration_statements: self.db.execute(text(statement)) self.db.execute( text( "UPDATE agent_assets " "SET working_version = COALESCE(working_version, current_version), " "published_version = CASE " "WHEN published_version IS NOT NULL THEN published_version " "WHEN status = 'active' THEN current_version " "ELSE published_version END" ) ) if migration_statements: self.db.commit()