323 lines
6.2 KiB
Python
323 lines
6.2 KiB
Python
|
|
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()
|