```
feat: 添加AI记忆模块和事件调度系统 - 新增AI记忆模块,支持本地记忆召回和自动写入功能 - 实现事件调度系统,支持批量处理待定事件和重试机制 - 集成心跳监控机制,跟踪API、调度器和工作节点状态 - 扩展仪表板数据统计,包含AI记忆条目和心跳概要 - 添加企业运营分析报告功能,提供财务、采购等多维度分析 - 更新配置设置,增加事件调度和AI记忆相关参数 - 优化任务队列,添加事件分发任务类型 - 扩展审计日志,记录AI记忆操作和事件调度行为 - 实现领域事件模型,支持事件持久化和状态管理 - 添加观察性服务,监控系统组件健康状况 ```
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user