create-windy 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +263 -121
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/apps/agent-server/adk_web_agents/canonical_debug/agent.py +29 -9
- package/template/apps/agent-server/adk_web_agents/services.py +25 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +40 -11
- package/template/apps/agent-server/src/southwind_agent_server/adk_debug_sessions.py +325 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +70 -6
- package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +1 -1
- package/template/apps/agent-server/src/southwind_agent_server/app.py +16 -4
- package/template/apps/agent-server/src/southwind_agent_server/approval_resolution.py +30 -0
- package/template/apps/agent-server/src/southwind_agent_server/contracts.py +9 -0
- package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +2 -1
- package/template/apps/agent-server/src/southwind_agent_server/engine.py +3 -1
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +95 -130
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine_validation.py +133 -0
- package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +89 -20
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_insert.py +34 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_queries.py +234 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_replay.py +44 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_status.py +83 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_store.py +331 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_summary.py +304 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_lifecycle.py +217 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_metrics.py +149 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_presenter.py +280 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_routes.py +126 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_store.py +344 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_types.py +111 -0
- package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +31 -2
- package/template/apps/agent-server/src/southwind_agent_server/service.py +56 -51
- package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +18 -1
- package/template/apps/agent-server/tests/test_adk_adapter.py +7 -9
- package/template/apps/agent-server/tests/test_adk_debug_sessions.py +215 -0
- package/template/apps/agent-server/tests/test_adk_web_canonical_debug.py +48 -1
- package/template/apps/agent-server/tests/test_openai_provider.py +58 -6
- package/template/apps/agent-server/tests/test_postgres_report_status.py +59 -0
- package/template/apps/agent-server/tests/test_postgres_report_summary.py +298 -0
- package/template/apps/agent-server/tests/test_provider_attempt_reporting.py +152 -0
- package/template/apps/agent-server/tests/test_report_attempt_lifecycle.py +126 -0
- package/template/apps/agent-server/tests/test_report_resilience.py +181 -0
- package/template/apps/agent-server/tests/test_report_store.py +286 -0
- package/template/apps/agent-server/tests/test_report_summary_parity.py +115 -0
- package/template/apps/agent-server/tests/test_report_terminal_sync.py +177 -0
- package/template/apps/server/src/agent/debug-session-facade-provider-failure.test.ts +269 -0
- package/template/apps/server/src/agent/debug-session-facade.test.ts +335 -0
- package/template/apps/server/src/agent/debug-session-facade.ts +302 -0
- package/template/apps/server/src/agent/debug-session-id.test.ts +37 -0
- package/template/apps/server/src/agent/debug-session-id.ts +37 -0
- package/template/apps/server/src/agent/debug-session-provider-reader.ts +54 -0
- package/template/apps/server/src/agent/debug-session-routes.test.ts +64 -0
- package/template/apps/server/src/agent/debug-session-routes.ts +73 -0
- package/template/apps/server/src/agent/debug-session-validation.ts +106 -0
- package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +2 -0
- package/template/apps/server/src/agent/provider-input-governance.test.ts +18 -0
- package/template/apps/server/src/agent/provider-input-governance.ts +37 -1
- package/template/apps/server/src/agent/remote-runtime.test.ts +7 -0
- package/template/apps/server/src/agent/remote-runtime.ts +1 -0
- package/template/apps/server/src/agent/run-authorization.test.ts +98 -0
- package/template/apps/server/src/agent/run-authorization.ts +32 -1
- package/template/apps/server/src/agent/run-binding-repository.ts +1 -0
- package/template/apps/server/src/agent/run-contracts.ts +5 -0
- package/template/apps/server/src/agent/run-facade-adk-debug.test.ts +39 -0
- package/template/apps/server/src/agent/run-facade.test.ts +78 -0
- package/template/apps/server/src/agent/run-facade.ts +33 -3
- package/template/apps/server/src/agent/run-input-commitment.ts +50 -0
- package/template/apps/server/src/agent/run-input-verification.ts +41 -0
- package/template/apps/server/src/agent/runtime-bootstrap-report.test.ts +136 -0
- package/template/apps/server/src/agent/runtime-bootstrap.ts +66 -1
- package/template/apps/server/src/agent/tool-host.test.ts +1 -0
- package/template/apps/server/src/agent/unavailable-routes.ts +4 -0
- package/template/apps/server/src/ai-runtime-report/actor-directory.ts +94 -0
- package/template/apps/server/src/ai-runtime-report/actor-handle.test.ts +27 -0
- package/template/apps/server/src/ai-runtime-report/actor-handle.ts +81 -0
- package/template/apps/server/src/ai-runtime-report/contracts.ts +91 -0
- package/template/apps/server/src/ai-runtime-report/remote-parser.ts +212 -0
- package/template/apps/server/src/ai-runtime-report/remote-port.ts +138 -0
- package/template/apps/server/src/ai-runtime-report/routes.ts +172 -0
- package/template/apps/server/src/ai-runtime-report/scope.ts +63 -0
- package/template/apps/server/src/ai-runtime-report/service.test.ts +271 -0
- package/template/apps/server/src/ai-runtime-report/service.ts +206 -0
- package/template/apps/server/src/license/runtime-audit-reliability.test.ts +2 -0
- package/template/apps/server/src/license/runtime-service.test.ts +6 -0
- package/template/apps/server/src/module-host/ai-operation-port.test.ts +63 -0
- package/template/apps/server/src/module-host/ai-operation-port.ts +30 -5
- package/template/apps/server/src/module-host/debug-session-provider-registration.test.ts +162 -0
- package/template/apps/server/src/module-host/debug-session-provider-registration.ts +84 -0
- package/template/apps/server/src/module-host/initialize-contracts.ts +7 -0
- package/template/apps/server/src/module-host/initialize.ts +6 -197
- package/template/apps/server/src/module-host/request-context.ts +1 -0
- package/template/apps/server/src/module-host/scoped-host.ts +214 -0
- package/template/apps/server/src/module-host.ts +1 -0
- package/template/apps/server/src/runtime-adk-debug.test.ts +25 -1
- package/template/apps/server/src/runtime-adk-debug.ts +8 -2
- package/template/apps/server/src/settings/drizzle-watermark-repository.integration.test.ts +4 -0
- package/template/apps/server/src/settings/drizzle-watermark-repository.ts +11 -3
- package/template/apps/server/src/settings/watermark-routes.test.ts +54 -1
- package/template/apps/server/src/settings/watermark-routes.ts +29 -1
- package/template/apps/server/src/system/operations.test.ts +54 -0
- package/template/apps/server/src/system/operations.ts +23 -1
- package/template/apps/server/src/system/seed.test.ts +4 -0
- package/template/apps/server/src/system/system-resource-monitoring.test.ts +82 -0
- package/template/apps/server/src/system/system-resource-monitoring.ts +300 -0
- package/template/apps/web/src/composables/useWatermarkSettings.webtest.ts +14 -0
- package/template/apps/web/src/composables/watermark-settings.ts +40 -11
- package/template/apps/web/src/composables/watermark-settings.webtest.ts +19 -6
- package/template/apps/web/src/layout/AppSidebar.vue +6 -1
- package/template/apps/web/src/layout/AppSidebar.webtest.ts +2 -0
- package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +2 -0
- package/template/apps/web/src/layout/GlobalWatermark.vue +4 -8
- package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +12 -5
- package/template/apps/web/src/pages/auth/LoginPage.vue +6 -1
- package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +20 -3
- package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +80 -0
- package/template/apps/web/src/pages/settings/WatermarkPreview.vue +72 -0
- package/template/apps/web/src/pages/settings/WatermarkPreview.webtest.ts +55 -0
- package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +80 -24
- package/template/apps/web/src/pages/settings/WatermarkSettingsSection.webtest.ts +115 -0
- package/template/apps/web/src/pages/system/AiRuntimePage.vue +129 -0
- package/template/apps/web/src/pages/system/SystemOperationsPage.vue +2 -1
- package/template/apps/web/src/pages/system/ai-runtime-presenter.ts +60 -0
- package/template/apps/web/src/pages/system/ai-runtime-presenter.webtest.ts +48 -0
- package/template/apps/web/src/pages/system/audit-time-range.ts +34 -2
- package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +4 -3
- package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +26 -1
- package/template/apps/web/src/pages/system/components/OperationGpuMonitor.vue +98 -0
- package/template/apps/web/src/pages/system/components/OperationGpuMonitor.webtest.ts +61 -0
- package/template/apps/web/src/pages/system/components/OperationServiceMonitor.vue +53 -0
- package/template/apps/web/src/pages/system/components/OperationServiceMonitor.webtest.ts +20 -1
- package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +14 -1
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeActivityChart.vue +85 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeCharts.webtest.ts +82 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +98 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +27 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue +168 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.vue +130 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.vue +82 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.webtest.ts +102 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeTokenByModelChart.vue +101 -0
- package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.ts +185 -0
- package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.webtest.ts +158 -0
- package/template/apps/web/src/pages/system/composables/useSystemResource.ts +21 -7
- package/template/apps/web/src/router/error-navigation.ts +22 -0
- package/template/apps/web/src/router/index.ts +12 -23
- package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
- package/template/apps/web/src/services/ai-runtime-api.ts +164 -0
- package/template/apps/web/src/services/ai-runtime-api.webtest.ts +111 -0
- package/template/apps/web/src/services/operations-api.ts +28 -0
- package/template/docker-compose.agent-debug.yml +1 -1
- package/template/docs/architecture/ai-runtime.md +2 -1
- package/template/docs/architecture/module-host.md +3 -0
- package/template/docs/platform/agent-run-attestation.md +65 -0
- package/template/docs/platform/agent-runtime-observability.md +135 -0
- package/template/docs/platform/agent-runtime.md +47 -3
- package/template/docs/platform/operations-overview.md +13 -0
- package/template/docs/platform/platform-authentication.md +4 -3
- package/template/docs/platform/platform-shell-settings.md +15 -10
- package/template/docs/platform/web-system-crud.md +1 -1
- package/template/packages/config/package.json +1 -1
- package/template/packages/database/drizzle/0044_sweet_tarantula.sql +6 -0
- package/template/packages/database/drizzle/0045_tiny_cassandra_nova.sql +65 -0
- package/template/packages/database/drizzle/0046_tranquil_living_mummy.sql +6 -0
- package/template/packages/database/drizzle/0047_oval_luke_cage.sql +18 -0
- package/template/packages/database/drizzle/meta/0044_snapshot.json +8349 -0
- package/template/packages/database/drizzle/meta/0045_snapshot.json +8814 -0
- package/template/packages/database/drizzle/meta/0046_snapshot.json +8820 -0
- package/template/packages/database/drizzle/meta/0047_snapshot.json +8835 -0
- package/template/packages/database/drizzle/meta/_journal.json +28 -0
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
- package/template/packages/database/src/schema/agent-runtime.ts +114 -0
- package/template/packages/database/src/schema/platform-settings.ts +3 -0
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/system-admin-routes.ts +10 -0
- package/template/packages/modules/src/system-admin-ui-catalog.ts +1 -1
- package/template/packages/modules/src/system-api-control-plane.ts +17 -1
- package/template/packages/modules/src/system-audit-actions.ts +10 -0
- package/template/packages/modules/src/system-features.ts +8 -0
- package/template/packages/modules/src/system-module-catalog.test.ts +35 -0
- package/template/packages/modules/src/system-module-catalog.ts +15 -4
- package/template/packages/modules/src/system-modules-access.test.ts +69 -0
- package/template/packages/modules/src/system-modules.ts +13 -0
- package/template/packages/modules/src/system-permissions.ts +2 -0
- package/template/packages/server-sdk/index.ts +11 -0
- package/template/packages/server-sdk/package.json +2 -1
- package/template/packages/server-sdk/src/ai-operation-port.ts +5 -0
- package/template/packages/server-sdk/src/debug-session-provider.ts +63 -0
- package/template/packages/server-sdk/src/module-host.ts +5 -0
- package/template/packages/shared/index.ts +3 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/ai-runtime-report.ts +134 -0
- package/template/packages/shared/src/platform-settings.ts +17 -0
- package/template/scripts/lib/postgres-regression.ts +201 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from .contracts import JsonValue
|
|
7
|
+
from .engine import EngineRunRequest, SafeEngineError
|
|
8
|
+
from .openai_provider import ProviderToolCall
|
|
9
|
+
from .report_types import AttemptTerminalOutcome
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def validate_runtime_request(
|
|
13
|
+
request: EngineRunRequest,
|
|
14
|
+
provider_deployment_key: str,
|
|
15
|
+
provider_capabilities: tuple[str, ...],
|
|
16
|
+
provider_input_modalities: tuple[str, ...],
|
|
17
|
+
) -> None:
|
|
18
|
+
if (
|
|
19
|
+
not request.instructions.strip()
|
|
20
|
+
or not request.user_prompt.strip()
|
|
21
|
+
or not request.execution_grant.strip()
|
|
22
|
+
or not request.tool_host_url.strip()
|
|
23
|
+
):
|
|
24
|
+
raise SafeEngineError(
|
|
25
|
+
"INVALID_INPUT",
|
|
26
|
+
"Agent Operation 运行计划不完整",
|
|
27
|
+
retryable=False,
|
|
28
|
+
)
|
|
29
|
+
if set(request.required_capabilities) - set(provider_capabilities):
|
|
30
|
+
raise SafeEngineError(
|
|
31
|
+
"MODEL_ROUTE_UNAVAILABLE",
|
|
32
|
+
"模型 Deployment 缺少 Operation 所需能力",
|
|
33
|
+
retryable=True,
|
|
34
|
+
)
|
|
35
|
+
if set(request.input_modalities) - set(provider_input_modalities):
|
|
36
|
+
raise SafeEngineError(
|
|
37
|
+
"MODEL_ROUTE_UNAVAILABLE",
|
|
38
|
+
"模型 Deployment 不支持 Operation 输入模态",
|
|
39
|
+
retryable=True,
|
|
40
|
+
)
|
|
41
|
+
if (
|
|
42
|
+
request.provider_egress.mode != "configured-endpoint"
|
|
43
|
+
or request.provider_egress.deployment_key != provider_deployment_key
|
|
44
|
+
):
|
|
45
|
+
raise SafeEngineError(
|
|
46
|
+
"FORBIDDEN",
|
|
47
|
+
"Agent Operation 未授权当前模型部署出网",
|
|
48
|
+
retryable=False,
|
|
49
|
+
)
|
|
50
|
+
if request.tool_network_policy.mode != "forbidden":
|
|
51
|
+
raise SafeEngineError(
|
|
52
|
+
"TOOL_DENIED",
|
|
53
|
+
"Agent Tool 网络策略不受支持",
|
|
54
|
+
retryable=False,
|
|
55
|
+
)
|
|
56
|
+
if (
|
|
57
|
+
not request.provider_data_policy.input_fields
|
|
58
|
+
or not request.provider_data_policy.envelope_fields
|
|
59
|
+
):
|
|
60
|
+
raise SafeEngineError(
|
|
61
|
+
"FORBIDDEN",
|
|
62
|
+
"Agent Operation 缺少 Provider 字段治理策略",
|
|
63
|
+
retryable=False,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def cancelled(event: asyncio.Event) -> None:
|
|
68
|
+
if event.is_set():
|
|
69
|
+
raise SafeEngineError(
|
|
70
|
+
"RUN_CANCELLED",
|
|
71
|
+
"Agent Run 已取消",
|
|
72
|
+
retryable=False,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def parse_arguments(raw: str) -> dict[str, JsonValue]:
|
|
77
|
+
try:
|
|
78
|
+
value = json.loads(raw or "{}")
|
|
79
|
+
except json.JSONDecodeError:
|
|
80
|
+
raise SafeEngineError(
|
|
81
|
+
"TOOL_DENIED",
|
|
82
|
+
"Agent Tool 参数无效",
|
|
83
|
+
retryable=False,
|
|
84
|
+
) from None
|
|
85
|
+
if not isinstance(value, dict):
|
|
86
|
+
raise SafeEngineError(
|
|
87
|
+
"TOOL_DENIED",
|
|
88
|
+
"Agent Tool 参数必须是对象",
|
|
89
|
+
retryable=False,
|
|
90
|
+
)
|
|
91
|
+
return value
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def assistant_tool_message(
|
|
95
|
+
calls: list[ProviderToolCall],
|
|
96
|
+
) -> dict[str, JsonValue]:
|
|
97
|
+
return {
|
|
98
|
+
"role": "assistant",
|
|
99
|
+
"content": None,
|
|
100
|
+
"tool_calls": [
|
|
101
|
+
{
|
|
102
|
+
"id": call.call_id,
|
|
103
|
+
"type": "function",
|
|
104
|
+
"function": {
|
|
105
|
+
"name": call.name,
|
|
106
|
+
"arguments": call.arguments,
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
for call in calls
|
|
110
|
+
],
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def validate_provider_tool_calls(
|
|
115
|
+
calls: list[ProviderToolCall],
|
|
116
|
+
allowed: set[str],
|
|
117
|
+
) -> None:
|
|
118
|
+
for call in calls:
|
|
119
|
+
if not call.call_id or not call.name or call.name not in allowed:
|
|
120
|
+
raise SafeEngineError(
|
|
121
|
+
"TOOL_DENIED",
|
|
122
|
+
"模型请求了未授权 Tool",
|
|
123
|
+
retryable=False,
|
|
124
|
+
)
|
|
125
|
+
parse_arguments(call.arguments)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def attempt_outcome(error: SafeEngineError) -> AttemptTerminalOutcome:
|
|
129
|
+
if error.code == "RUN_CANCELLED":
|
|
130
|
+
return "cancelled"
|
|
131
|
+
if error.code in {"FORBIDDEN", "INVALID_INPUT", "TOOL_DENIED"}:
|
|
132
|
+
return "blocked"
|
|
133
|
+
return "failed"
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass, field, replace
|
|
5
6
|
from typing import cast
|
|
6
7
|
|
|
7
8
|
import httpx
|
|
@@ -13,6 +14,7 @@ from .config import (
|
|
|
13
14
|
from .contracts import AgentErrorCode, JsonValue, RuntimeToolDefinition
|
|
14
15
|
from .engine import SafeEngineError
|
|
15
16
|
from .multimodal import response_format
|
|
17
|
+
from .report_types import UsageFacts
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
@dataclass
|
|
@@ -26,7 +28,8 @@ class ProviderToolCall:
|
|
|
26
28
|
class ProviderTurn:
|
|
27
29
|
text_deltas: list[str] = field(default_factory=list)
|
|
28
30
|
tool_calls: list[ProviderToolCall] = field(default_factory=list)
|
|
29
|
-
usage:
|
|
31
|
+
usage: UsageFacts = field(default_factory=UsageFacts)
|
|
32
|
+
reported_model: str | None = None
|
|
30
33
|
|
|
31
34
|
@property
|
|
32
35
|
def text(self) -> str:
|
|
@@ -69,6 +72,7 @@ class OpenAiCompatibleProvider:
|
|
|
69
72
|
output_schema,
|
|
70
73
|
)
|
|
71
74
|
turn = ProviderTurn()
|
|
75
|
+
started_at = time.perf_counter()
|
|
72
76
|
async with self._client() as client:
|
|
73
77
|
try:
|
|
74
78
|
async with client.stream(
|
|
@@ -80,7 +84,7 @@ class OpenAiCompatibleProvider:
|
|
|
80
84
|
if not response.is_success:
|
|
81
85
|
await self._bounded_stream(response)
|
|
82
86
|
raise provider_status_failure(response.status_code)
|
|
83
|
-
await self._consume_stream(response, turn)
|
|
87
|
+
await self._consume_stream(response, turn, started_at)
|
|
84
88
|
except SafeEngineError:
|
|
85
89
|
raise
|
|
86
90
|
except httpx.HTTPError as error:
|
|
@@ -135,6 +139,7 @@ class OpenAiCompatibleProvider:
|
|
|
135
139
|
self,
|
|
136
140
|
response: httpx.Response,
|
|
137
141
|
turn: ProviderTurn,
|
|
142
|
+
started_at: float,
|
|
138
143
|
) -> None:
|
|
139
144
|
consumed = 0
|
|
140
145
|
async for line in response.aiter_lines():
|
|
@@ -152,7 +157,7 @@ class OpenAiCompatibleProvider:
|
|
|
152
157
|
continue
|
|
153
158
|
try:
|
|
154
159
|
chunk = json.loads(data)
|
|
155
|
-
consume_chunk(chunk, turn)
|
|
160
|
+
consume_chunk(chunk, turn, started_at)
|
|
156
161
|
except (KeyError, TypeError, ValueError, json.JSONDecodeError):
|
|
157
162
|
raise SafeEngineError(
|
|
158
163
|
"PROVIDER_UNAVAILABLE",
|
|
@@ -179,12 +184,23 @@ class OpenAiCompatibleProvider:
|
|
|
179
184
|
return body
|
|
180
185
|
|
|
181
186
|
|
|
182
|
-
def consume_chunk(
|
|
187
|
+
def consume_chunk(
|
|
188
|
+
value: object,
|
|
189
|
+
turn: ProviderTurn,
|
|
190
|
+
started_at: float | None = None,
|
|
191
|
+
) -> None:
|
|
183
192
|
if not isinstance(value, dict):
|
|
184
193
|
raise TypeError
|
|
194
|
+
reported_model = value.get("model")
|
|
195
|
+
if isinstance(reported_model, str) and reported_model:
|
|
196
|
+
turn.reported_model = reported_model
|
|
185
197
|
usage = value.get("usage")
|
|
186
198
|
if isinstance(usage, dict):
|
|
187
|
-
|
|
199
|
+
measured = usage_value(usage)
|
|
200
|
+
turn.usage = replace(
|
|
201
|
+
measured,
|
|
202
|
+
first_token_latency_ms=turn.usage.first_token_latency_ms,
|
|
203
|
+
)
|
|
188
204
|
choices = value.get("choices")
|
|
189
205
|
if not isinstance(choices, list) or not choices:
|
|
190
206
|
return
|
|
@@ -196,6 +212,7 @@ def consume_chunk(value: object, turn: ProviderTurn) -> None:
|
|
|
196
212
|
return
|
|
197
213
|
content = delta.get("content")
|
|
198
214
|
if isinstance(content, str) and content:
|
|
215
|
+
observe_first_token(turn, started_at)
|
|
199
216
|
turn.text_deltas.append(content)
|
|
200
217
|
raw_calls = delta.get("tool_calls")
|
|
201
218
|
if not isinstance(raw_calls, list):
|
|
@@ -208,6 +225,7 @@ def consume_chunk(value: object, turn: ProviderTurn) -> None:
|
|
|
208
225
|
raise TypeError
|
|
209
226
|
while len(turn.tool_calls) <= index:
|
|
210
227
|
turn.tool_calls.append(ProviderToolCall())
|
|
228
|
+
observe_first_token(turn, started_at)
|
|
211
229
|
call = turn.tool_calls[index]
|
|
212
230
|
call_id = raw.get("id")
|
|
213
231
|
if isinstance(call_id, str):
|
|
@@ -236,21 +254,72 @@ def tool_schema(tool: RuntimeToolDefinition) -> JsonValue:
|
|
|
236
254
|
}
|
|
237
255
|
|
|
238
256
|
|
|
239
|
-
def usage_value(raw: dict[object, object]) ->
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
257
|
+
def usage_value(raw: dict[object, object]) -> UsageFacts:
|
|
258
|
+
input_tokens = non_negative_integer(raw.get("prompt_tokens"))
|
|
259
|
+
output_tokens = non_negative_integer(raw.get("completion_tokens"))
|
|
260
|
+
total_tokens = non_negative_integer(raw.get("total_tokens"))
|
|
261
|
+
prompt_details = raw.get("prompt_tokens_details")
|
|
262
|
+
cached_tokens = (
|
|
263
|
+
non_negative_integer(prompt_details.get("cached_tokens"))
|
|
264
|
+
if isinstance(prompt_details, dict)
|
|
265
|
+
else None
|
|
266
|
+
)
|
|
267
|
+
if (
|
|
268
|
+
cached_tokens is not None
|
|
269
|
+
and input_tokens is not None
|
|
270
|
+
and cached_tokens > input_tokens
|
|
271
|
+
):
|
|
272
|
+
cached_tokens = None
|
|
273
|
+
completion_details = raw.get("completion_tokens_details")
|
|
274
|
+
reasoning_tokens = (
|
|
275
|
+
non_negative_integer(completion_details.get("reasoning_tokens"))
|
|
276
|
+
if isinstance(completion_details, dict)
|
|
277
|
+
else None
|
|
278
|
+
)
|
|
279
|
+
known = any(
|
|
280
|
+
value is not None
|
|
281
|
+
for value in (
|
|
282
|
+
input_tokens,
|
|
283
|
+
output_tokens,
|
|
284
|
+
cached_tokens,
|
|
285
|
+
reasoning_tokens,
|
|
286
|
+
total_tokens,
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
return UsageFacts(
|
|
290
|
+
source="provider-reported" if known else "unknown",
|
|
291
|
+
input_tokens=input_tokens,
|
|
292
|
+
output_tokens=output_tokens,
|
|
293
|
+
cached_input_tokens=cached_tokens,
|
|
294
|
+
reasoning_tokens=reasoning_tokens,
|
|
295
|
+
total_tokens=total_tokens,
|
|
296
|
+
cache_status=(
|
|
297
|
+
"unknown"
|
|
298
|
+
if cached_tokens is None
|
|
299
|
+
else "hit"
|
|
300
|
+
if cached_tokens > 0
|
|
301
|
+
else "miss"
|
|
302
|
+
),
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def non_negative_integer(value: object) -> int | None:
|
|
250
307
|
return (
|
|
251
|
-
|
|
252
|
-
if
|
|
253
|
-
else
|
|
308
|
+
value
|
|
309
|
+
if isinstance(value, int) and not isinstance(value, bool) and value >= 0
|
|
310
|
+
else None
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def observe_first_token(turn: ProviderTurn, started_at: float | None) -> None:
|
|
315
|
+
if started_at is None or turn.usage.first_token_latency_ms is not None:
|
|
316
|
+
return
|
|
317
|
+
turn.usage = replace(
|
|
318
|
+
turn.usage,
|
|
319
|
+
first_token_latency_ms=max(
|
|
320
|
+
0,
|
|
321
|
+
round((time.perf_counter() - started_at) * 1_000),
|
|
322
|
+
),
|
|
254
323
|
)
|
|
255
324
|
|
|
256
325
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .contracts import StartRunRequest
|
|
4
|
+
from .report_types import retention_seconds
|
|
5
|
+
|
|
6
|
+
RUN_INSERT_SQL = """
|
|
7
|
+
insert into agent_runtime_report_runs (
|
|
8
|
+
run_id, operation_key, release_id, trace_id,
|
|
9
|
+
actor_id, actor_type, department_id, status,
|
|
10
|
+
created_at, updated_at, expires_at
|
|
11
|
+
) values (%s, %s, %s, %s, %s, %s, %s, 'accepted', %s, %s, %s)
|
|
12
|
+
on conflict (run_id) do nothing
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run_insert_values(
|
|
17
|
+
run_id: str,
|
|
18
|
+
request: StartRunRequest,
|
|
19
|
+
created_at: float,
|
|
20
|
+
retention_days: int,
|
|
21
|
+
) -> tuple[object, ...]:
|
|
22
|
+
context = request.reporting
|
|
23
|
+
return (
|
|
24
|
+
run_id,
|
|
25
|
+
request.operation_key,
|
|
26
|
+
request.release.release_id,
|
|
27
|
+
request.trace_id,
|
|
28
|
+
context.actor_id if context else None,
|
|
29
|
+
context.actor_type if context else None,
|
|
30
|
+
context.department_id if context else None,
|
|
31
|
+
created_at,
|
|
32
|
+
created_at,
|
|
33
|
+
created_at + retention_seconds(retention_days),
|
|
34
|
+
)
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, cast
|
|
4
|
+
|
|
5
|
+
from psycopg import AsyncConnection
|
|
6
|
+
from psycopg_pool import AsyncConnectionPool
|
|
7
|
+
|
|
8
|
+
from .report_presenter import (
|
|
9
|
+
attempt_value,
|
|
10
|
+
attempts_for,
|
|
11
|
+
run_value,
|
|
12
|
+
)
|
|
13
|
+
from .report_types import (
|
|
14
|
+
AttemptOutcome,
|
|
15
|
+
AttemptReport,
|
|
16
|
+
CacheStatus,
|
|
17
|
+
ReportFilter,
|
|
18
|
+
RunReport,
|
|
19
|
+
UsageFacts,
|
|
20
|
+
UsageSource,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
type DatabaseConnection = AsyncConnection[dict[str, Any]]
|
|
24
|
+
type DatabasePool = AsyncConnectionPool[DatabaseConnection]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def list_runs(
|
|
28
|
+
pool: DatabasePool,
|
|
29
|
+
query: ReportFilter,
|
|
30
|
+
page: int,
|
|
31
|
+
page_size: int,
|
|
32
|
+
now: float,
|
|
33
|
+
) -> dict[str, object]:
|
|
34
|
+
clause, parameters = filter_clause(query, now)
|
|
35
|
+
async with pool.connection() as connection:
|
|
36
|
+
count_cursor = await connection.execute(
|
|
37
|
+
f"select count(*) as total from agent_runtime_report_runs {clause}",
|
|
38
|
+
parameters,
|
|
39
|
+
)
|
|
40
|
+
count_row = await count_cursor.fetchone()
|
|
41
|
+
cursor = await connection.execute(
|
|
42
|
+
f"""
|
|
43
|
+
select * from agent_runtime_report_runs {clause}
|
|
44
|
+
order by created_at desc limit %s offset %s
|
|
45
|
+
""",
|
|
46
|
+
(*parameters, page_size, (page - 1) * page_size),
|
|
47
|
+
)
|
|
48
|
+
runs = [run_from_row(row) for row in await cursor.fetchall()]
|
|
49
|
+
attempts = await select_attempts(
|
|
50
|
+
connection,
|
|
51
|
+
[run.run_id for run in runs],
|
|
52
|
+
)
|
|
53
|
+
return {
|
|
54
|
+
"items": [run_value(run, attempts_for(attempts, [run])) for run in runs],
|
|
55
|
+
"total": int(count_row["total"]) if count_row else 0,
|
|
56
|
+
"page": page,
|
|
57
|
+
"pageSize": page_size,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def get_run(
|
|
62
|
+
pool: DatabasePool,
|
|
63
|
+
run_id: str,
|
|
64
|
+
now: float,
|
|
65
|
+
) -> dict[str, object] | None:
|
|
66
|
+
async with pool.connection() as connection:
|
|
67
|
+
cursor = await connection.execute(
|
|
68
|
+
"""
|
|
69
|
+
select * from agent_runtime_report_runs
|
|
70
|
+
where run_id = %s and expires_at > %s
|
|
71
|
+
""",
|
|
72
|
+
(run_id, now),
|
|
73
|
+
)
|
|
74
|
+
row = await cursor.fetchone()
|
|
75
|
+
if not row:
|
|
76
|
+
return None
|
|
77
|
+
run = run_from_row(row)
|
|
78
|
+
attempts = await select_attempts(connection, [run_id])
|
|
79
|
+
return {
|
|
80
|
+
**run_value(run, attempts),
|
|
81
|
+
"releaseId": run.release_id,
|
|
82
|
+
"traceId": run.trace_id,
|
|
83
|
+
"safeErrorCode": run.safe_error_code,
|
|
84
|
+
"attempts": [attempt_value(item) for item in attempts],
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def select_attempts(
|
|
89
|
+
connection: DatabaseConnection,
|
|
90
|
+
run_ids: list[str],
|
|
91
|
+
) -> list[AttemptReport]:
|
|
92
|
+
if not run_ids:
|
|
93
|
+
return []
|
|
94
|
+
cursor = await connection.execute(
|
|
95
|
+
"""
|
|
96
|
+
select * from agent_runtime_report_attempts
|
|
97
|
+
where run_id = any(%s) order by run_id, sequence
|
|
98
|
+
""",
|
|
99
|
+
(run_ids,),
|
|
100
|
+
)
|
|
101
|
+
return [attempt_from_row(row) for row in await cursor.fetchall()]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def filter_clause(
|
|
105
|
+
query: ReportFilter,
|
|
106
|
+
now: float,
|
|
107
|
+
alias: str = "",
|
|
108
|
+
) -> tuple[str, tuple[object, ...]]:
|
|
109
|
+
prefix = f"{alias}." if alias else ""
|
|
110
|
+
conditions = [
|
|
111
|
+
f"{prefix}created_at >= %s",
|
|
112
|
+
f"{prefix}created_at <= %s",
|
|
113
|
+
f"{prefix}expires_at > %s",
|
|
114
|
+
]
|
|
115
|
+
parameters: list[object] = [
|
|
116
|
+
query.from_timestamp,
|
|
117
|
+
query.to_timestamp,
|
|
118
|
+
now,
|
|
119
|
+
]
|
|
120
|
+
scoped_conditions, scoped_parameters = scope_conditions(
|
|
121
|
+
query,
|
|
122
|
+
prefix,
|
|
123
|
+
)
|
|
124
|
+
conditions.extend(scoped_conditions)
|
|
125
|
+
parameters.extend(scoped_parameters)
|
|
126
|
+
return f"where {' and '.join(conditions)}", tuple(parameters)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def attempt_filter_clause(
|
|
130
|
+
query: ReportFilter,
|
|
131
|
+
now: float,
|
|
132
|
+
run_alias: str = "r",
|
|
133
|
+
attempt_alias: str = "a",
|
|
134
|
+
) -> tuple[str, tuple[object, ...]]:
|
|
135
|
+
run_prefix = f"{run_alias}." if run_alias else ""
|
|
136
|
+
attempt_prefix = f"{attempt_alias}." if attempt_alias else ""
|
|
137
|
+
conditions = [
|
|
138
|
+
f"{attempt_prefix}started_at >= %s",
|
|
139
|
+
f"{attempt_prefix}started_at <= %s",
|
|
140
|
+
f"{attempt_prefix}finished_at is not null",
|
|
141
|
+
f"{run_prefix}expires_at > %s",
|
|
142
|
+
]
|
|
143
|
+
parameters: list[object] = [
|
|
144
|
+
query.from_timestamp,
|
|
145
|
+
query.to_timestamp,
|
|
146
|
+
now,
|
|
147
|
+
]
|
|
148
|
+
scoped_conditions, scoped_parameters = scope_conditions(
|
|
149
|
+
query,
|
|
150
|
+
run_prefix,
|
|
151
|
+
)
|
|
152
|
+
conditions.extend(scoped_conditions)
|
|
153
|
+
parameters.extend(scoped_parameters)
|
|
154
|
+
return f"where {' and '.join(conditions)}", tuple(parameters)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def scope_conditions(
|
|
158
|
+
query: ReportFilter,
|
|
159
|
+
prefix: str,
|
|
160
|
+
) -> tuple[list[str], list[object]]:
|
|
161
|
+
conditions: list[str] = []
|
|
162
|
+
parameters: list[object] = []
|
|
163
|
+
if query.selected_actor_id is not None:
|
|
164
|
+
conditions.append(f"{prefix}actor_id = %s")
|
|
165
|
+
parameters.append(query.selected_actor_id)
|
|
166
|
+
if query.scope_restricted:
|
|
167
|
+
scopes: list[str] = []
|
|
168
|
+
if query.actor_ids:
|
|
169
|
+
scopes.append(f"{prefix}actor_id = any(%s)")
|
|
170
|
+
parameters.append(list(query.actor_ids))
|
|
171
|
+
if query.department_ids:
|
|
172
|
+
scopes.append(f"{prefix}department_id = any(%s)")
|
|
173
|
+
parameters.append(list(query.department_ids))
|
|
174
|
+
conditions.append(f"({' or '.join(scopes)})" if scopes else "false")
|
|
175
|
+
return conditions, parameters
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def run_from_row(row: dict[str, Any]) -> RunReport:
|
|
179
|
+
return RunReport(
|
|
180
|
+
run_id=str(row["run_id"]),
|
|
181
|
+
operation_key=str(row["operation_key"]),
|
|
182
|
+
release_id=str(row["release_id"]),
|
|
183
|
+
trace_id=str(row["trace_id"]),
|
|
184
|
+
actor_id=optional_text(row.get("actor_id")),
|
|
185
|
+
actor_type=optional_text(row.get("actor_type")),
|
|
186
|
+
department_id=optional_text(row.get("department_id")),
|
|
187
|
+
status=cast(Any, str(row["status"])),
|
|
188
|
+
safe_error_code=optional_text(row.get("safe_error_code")),
|
|
189
|
+
started_at=optional_float(row.get("started_at")),
|
|
190
|
+
finished_at=optional_float(row.get("finished_at")),
|
|
191
|
+
duration_ms=optional_int(row.get("duration_ms")),
|
|
192
|
+
created_at=float(row["created_at"]),
|
|
193
|
+
updated_at=float(row["updated_at"]),
|
|
194
|
+
expires_at=float(row["expires_at"]),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def attempt_from_row(row: dict[str, Any]) -> AttemptReport:
|
|
199
|
+
return AttemptReport(
|
|
200
|
+
attempt_id=str(row["attempt_id"]),
|
|
201
|
+
run_id=str(row["run_id"]),
|
|
202
|
+
sequence=int(row["sequence"]),
|
|
203
|
+
tool_round=int(row["tool_round"]),
|
|
204
|
+
deployment_key=str(row["deployment_key"]),
|
|
205
|
+
requested_model=str(row["requested_model"]),
|
|
206
|
+
reported_model=optional_text(row.get("reported_model")),
|
|
207
|
+
outcome=cast(AttemptOutcome, str(row["outcome"])),
|
|
208
|
+
usage=UsageFacts(
|
|
209
|
+
source=cast(UsageSource, str(row["usage_source"])),
|
|
210
|
+
input_tokens=optional_int(row.get("input_tokens")),
|
|
211
|
+
output_tokens=optional_int(row.get("output_tokens")),
|
|
212
|
+
cached_input_tokens=optional_int(row.get("cached_input_tokens")),
|
|
213
|
+
reasoning_tokens=optional_int(row.get("reasoning_tokens")),
|
|
214
|
+
total_tokens=optional_int(row.get("total_tokens")),
|
|
215
|
+
first_token_latency_ms=optional_int(row.get("first_token_latency_ms")),
|
|
216
|
+
cache_status=cast(CacheStatus, str(row["cache_status"])),
|
|
217
|
+
),
|
|
218
|
+
duration_ms=optional_int(row.get("duration_ms")),
|
|
219
|
+
safe_error_code=optional_text(row.get("safe_error_code")),
|
|
220
|
+
started_at=float(row["started_at"]),
|
|
221
|
+
finished_at=optional_float(row.get("finished_at")),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def optional_text(value: object) -> str | None:
|
|
226
|
+
return str(value) if value is not None else None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def optional_float(value: Any) -> float | None:
|
|
230
|
+
return float(value) if value is not None else None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def optional_int(value: Any) -> int | None:
|
|
234
|
+
return int(value) if value is not None else None
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, cast
|
|
4
|
+
|
|
5
|
+
from .report_types import (
|
|
6
|
+
AttemptTerminalOutcome,
|
|
7
|
+
CacheStatus,
|
|
8
|
+
UsageFacts,
|
|
9
|
+
UsageSource,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def assert_attempt_terminal_replay(
|
|
14
|
+
current: dict[str, Any],
|
|
15
|
+
outcome: AttemptTerminalOutcome,
|
|
16
|
+
usage: UsageFacts,
|
|
17
|
+
reported_model: str | None,
|
|
18
|
+
safe_error_code: str | None,
|
|
19
|
+
) -> None:
|
|
20
|
+
current_usage = UsageFacts(
|
|
21
|
+
source=cast(UsageSource, str(current["usage_source"])),
|
|
22
|
+
input_tokens=optional_int(current["input_tokens"]),
|
|
23
|
+
output_tokens=optional_int(current["output_tokens"]),
|
|
24
|
+
cached_input_tokens=optional_int(current["cached_input_tokens"]),
|
|
25
|
+
reasoning_tokens=optional_int(current["reasoning_tokens"]),
|
|
26
|
+
total_tokens=optional_int(current["total_tokens"]),
|
|
27
|
+
first_token_latency_ms=optional_int(current["first_token_latency_ms"]),
|
|
28
|
+
cache_status=cast(CacheStatus, str(current["cache_status"])),
|
|
29
|
+
)
|
|
30
|
+
if (
|
|
31
|
+
current["outcome"] != outcome
|
|
32
|
+
or current_usage != usage
|
|
33
|
+
or optional_text(current["reported_model"]) != reported_model
|
|
34
|
+
or optional_text(current["safe_error_code"]) != safe_error_code
|
|
35
|
+
):
|
|
36
|
+
raise ValueError("Agent Runtime 报表 Attempt 终态冲突")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def optional_text(value: object) -> str | None:
|
|
40
|
+
return str(value) if value is not None else None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def optional_int(value: Any) -> int | None:
|
|
44
|
+
return int(value) if value is not None else None
|