feat: add Nailong Fund advisor capabilities

This commit is contained in:
Windows
2026-09-11 09:47:40 +08:00
parent 5907fcd6d2
commit a4499defee
143 changed files with 12255 additions and 89 deletions
+46 -4
View File
@@ -12,6 +12,7 @@ from app.core.config import Settings, get_settings
from app.core.contracts import AgentRequest, AgentRequestMetadata, AgentResult, RequestContext
from app.core.errors import AgentError, RecoverableAgentError, RunLeaseLostError
from app.infrastructure.db import SessionFactory
from app.infrastructure.neo4j_graph_driver import Neo4jGraphDriver
from app.model.audit import InteractionAudit
from app.model.conversation import ConversationMessage
from app.model.platform import AgentRun, DomainEventOutbox, RequestIdempotency
@@ -22,7 +23,14 @@ from app.service.agent.factory import AgentFactory
from app.service.agent_persistence_service import AgentPersistenceService
from app.service.identity_service import IdentityService
from app.service.memory_service import MemoryService
from app.service.portfolio_projection_scheduler import PortfolioProjectionScheduler
from app.service.relationship_service import RelationshipService
from app.worker.outbox_worker import OutboxWorker
from app.worker.portfolio_graph_projection_worker import PortfolioGraphProjectionWorker
from app.worker.product_governance_monitor_worker import ProductGovernanceMonitorWorker
from app.worker.product_history_sync_worker import ProductHistorySyncWorker
from app.worker.product_market_quote_sync_worker import ProductMarketQuoteSyncWorker
from app.worker.product_metric_refresh_worker import ProductMetricRefreshWorker
logger = logging.getLogger(__name__)
@@ -35,6 +43,14 @@ class WorkerRuntime:
self.factory = factory if factory is not None else get_agent_factory()
self.settings = settings or get_settings()
self.resolve_identity = resolve_identity or IdentityService().resolve
self.portfolio_projection_scheduler = PortfolioProjectionScheduler()
self.portfolio_projection_worker = PortfolioGraphProjectionWorker(
RelationshipService(Neo4jGraphDriver(self.settings))
)
self.product_history_sync_worker = ProductHistorySyncWorker()
self.product_governance_monitor_worker = ProductGovernanceMonitorWorker()
self.product_market_quote_sync_worker = ProductMarketQuoteSyncWorker()
self.product_metric_refresh_worker = ProductMetricRefreshWorker()
async def dispatch_one(self, *, run_id: str | None = None) -> bool:
# Outbox acknowledges a durable SQL queue entry, not an in-memory task.
@@ -44,10 +60,23 @@ class WorkerRuntime:
if run is None:
raise ValueError("run not found")
return await OutboxWorker(session, {"agent.run_requested": dispatch}).publish_one(
aggregate_id=run_id)
async def project_portfolio(payload: dict[str, Any]) -> None:
customer_id = payload.get("customer_id")
if type(customer_id) is not int or customer_id <= 0:
raise ValueError("invalid portfolio projection customer")
await self.portfolio_projection_worker.project_customer(customer_id)
return await OutboxWorker(session, {
"agent.run_requested": dispatch,
"portfolio.projection_requested": project_portfolio,
}).publish_one(aggregate_id=run_id)
async def run_once(self) -> bool:
governance_changes = await self.product_governance_monitor_worker.refresh_once()
quotes_refreshed = await self.product_market_quote_sync_worker.refresh_once()
history_refreshed = await self.product_history_sync_worker.refresh_once()
metrics_refreshed = await self.product_metric_refresh_worker.refresh_once()
projection_enqueued = await self.portfolio_projection_scheduler.enqueue_changes()
dispatched = await self.dispatch_one()
now = datetime.now(UTC).replace(tzinfo=None)
async with SessionFactory() as session:
@@ -56,8 +85,21 @@ class WorkerRuntime:
(AgentRun.locked_until.is_(None) | (AgentRun.locked_until < now)),
).order_by(AgentRun.created_at).limit(1))
if run_id is None:
return dispatched
return await self.execute(run_id) or dispatched
return any((
dispatched,
bool(projection_enqueued),
bool(metrics_refreshed),
bool(history_refreshed),
bool(quotes_refreshed),
bool(governance_changes),
))
return (
await self.execute(run_id)
or dispatched
or bool(projection_enqueued)
or bool(metrics_refreshed)
or bool(history_refreshed)
)
async def execute(self, run_id: str) -> bool:
# A new fencing token for each claim also fences restarts of the same process.