feat: 添加AI记忆模块和事件调度系统

- 新增AI记忆模块,支持本地记忆召回和自动写入功能
- 实现事件调度系统,支持批量处理待定事件和重试机制
- 集成心跳监控机制,跟踪API、调度器和工作节点状态
- 扩展仪表板数据统计,包含AI记忆条目和心跳概要
- 添加企业运营分析报告功能,提供财务、采购等多维度分析
- 更新配置设置,增加事件调度和AI记忆相关参数
- 优化任务队列,添加事件分发任务类型
- 扩展审计日志,记录AI记忆操作和事件调度行为
- 实现领域事件模型,支持事件持久化和状态管理
- 添加观察性服务,监控系统组件健康状况
```
This commit is contained in:
2026-07-09 17:26:19 +08:00
parent 0a153b264a
commit 0cda45238a
33 changed files with 1591 additions and 34 deletions

View File

@@ -35,6 +35,11 @@ from app.core.pagination import bounded_limit, bounded_offset
from app.core.security import require_api_key, require_audit_api_key
from app.main import _allow_cors_credentials, app
from app.modules.audit.constants import AUDIT_REDACTED_VALUE
from app.modules.ai_memory.constants import (
AIMemoryPayloadKey,
AIMemoryResponseKey,
AIMemoryStatus,
)
from app.modules.events.constants import (
EventAggregateType,
EventPayloadKey,
@@ -46,7 +51,13 @@ from app.modules.events.service import EventService
from app.modules.business.registry import get_domain_model
from app.modules.business.service import _model_payload, serialize_model
from app.modules.legacy_mysql.service import LegacyMySQLService
from app.modules.observability.constants import (
HeartbeatComponent,
ObservabilityKey,
)
from app.modules.observability.service import ObservabilityService
from app.modules.reports.constants import (
EnterpriseAnalyticsKey,
LifecycleAttentionKey,
LifecycleResponseKey,
LifecycleSection,
@@ -168,6 +179,16 @@ def test_feishu_webhook_routes_message_event() -> None:
def test_v3_request_id_health_and_metrics() -> None:
db = SessionLocal()
try:
ObservabilityService(db).record_heartbeat(
component=HeartbeatComponent.WORKER,
instance_id="pytest-worker",
actor="pytest",
)
finally:
db.close()
response = client.get("/api/v1/health/live", headers={"X-Request-ID": "rid-v3-smoke"})
assert response.status_code == 200
assert response.headers["X-Request-ID"] == "rid-v3-smoke"
@@ -179,7 +200,9 @@ def test_v3_request_id_health_and_metrics() -> None:
response = client.get("/api/v1/metrics", headers=headers)
assert response.status_code == 200
assert "metrics" in response.json()
metrics = response.json()["metrics"]
assert ObservabilityKey.EVENTS in metrics
assert ObservabilityKey.HEARTBEATS in metrics
def test_v3_event_idempotency_and_workflow_dispatch() -> None:
@@ -228,6 +251,80 @@ def test_v3_event_idempotency_and_workflow_dispatch() -> None:
db.close()
def test_v3_event_retry_and_dispatch_pending_route() -> None:
db = SessionLocal()
try:
event = EventService(db).emit(
event_type=EventType.REPORT_PUSH_FAILED,
source=EventSource.REPORTS,
aggregate_type=EventAggregateType.REPORT_PUSH_RUN,
aggregate_id="push-v3-retry",
actor="pytest",
payload={
EventPayloadKey.CODE: "push-v3-retry",
EventPayloadKey.STATUS: ReportPushStatus.FAILED,
},
idempotency_key="v3-report-push-retry",
)
event.status = EventStatus.FAILED
event.last_error = "transient"
db.commit()
event_id = event.event_id
finally:
db.close()
retry_response = client.post(f"/api/v1/events/{event_id}/retry", headers=headers)
assert retry_response.status_code == 200
assert retry_response.json()["event"]["status"] == EventStatus.PENDING
dispatch_response = client.post("/api/v1/events/dispatch-pending", headers=headers)
assert dispatch_response.status_code == 200
dispatched = [
item for item in dispatch_response.json()["items"] if item["event_id"] == event_id
]
assert dispatched
assert dispatched[0]["status"] == EventStatus.PROCESSED
def test_v3_ai_memory_recall_and_auto_write() -> None:
response = client.post(
"/api/v1/ai/ask",
headers=headers,
json={
"prompt": "Summarize quarterly cash planning for project memory smoke",
"context": {
AIMemoryPayloadKey.SCOPE: "project",
AIMemoryPayloadKey.SUBJECT: "P-MEM-SMOKE",
},
},
)
assert response.status_code == 200
data = response.json()
assert data[AIResponseKey.PROVIDER] == AIProviderName.NOOP
assert data[AIResponseKey.RAW][AIResponseKey.MEMORY_WRITE][AIMemoryPayloadKey.STATUS] == (
AIMemoryStatus.ACTIVE
)
list_response = client.get(
"/api/v1/ai/memory?scope=project&subject=P-MEM-SMOKE",
headers=headers,
)
assert list_response.status_code == 200
assert list_response.json()[AIMemoryResponseKey.ITEMS]
recall_response = client.post(
"/api/v1/ai/memory/recall",
headers=headers,
json={
"query": "quarterly cash planning",
"scope": "project",
"subject": "P-MEM-SMOKE",
},
)
assert recall_response.status_code == 200
assert recall_response.json()[AIMemoryResponseKey.ITEMS]
def test_v3_risk_action_routes_are_disabled_in_read_only_mode() -> None:
response = create_business_record(
"risk-events",
@@ -648,6 +745,43 @@ def test_project_lifecycle_report_summarizes_progress_cost_and_risk() -> None:
)
def test_v3_enterprise_analytics_returns_read_only_sections() -> None:
performance_response = create_business_record(
"performance-metrics",
{
"code": "PERF-V3-001",
"name": "V3 delivery score",
"weight": 20,
"auto_score": 82,
"confirmed_score": 78,
"status": StatusValue.REVIEWED,
},
)
assert performance_response.status_code == 200
response = client.get("/api/v1/reports/enterprise-analytics", headers=headers)
assert response.status_code == 200
data = response.json()
assert data[EnterpriseAnalyticsKey.TITLE] == ReportTitle.ENTERPRISE_ANALYTICS
assert EnterpriseAnalyticsKey.FINANCE in data
assert EnterpriseAnalyticsKey.PROCUREMENT in data
assert EnterpriseAnalyticsKey.PERFORMANCE in data
assert EnterpriseAnalyticsKey.OPERATIONS in data
assert data[EnterpriseAnalyticsKey.PERFORMANCE][MetricKey.CONFIRMED] >= 1
db = SessionLocal()
try:
workflow = db.execute(
select(WorkflowInstance).where(
WorkflowInstance.workflow_type == WorkflowType.ENTERPRISE_ANALYTICS,
WorkflowInstance.aggregate_id == data[EnterpriseAnalyticsKey.CODE],
)
).scalar_one()
assert workflow.status == WorkflowStatus.COMPLETED
finally:
db.close()
def test_work_report_counts_pending_approval_backlog_outside_period() -> None:
today = date.today()
project_code = "P-BACKLOG-001"