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.
Files changed (194) hide show
  1. package/dist/cli.js +263 -121
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/apps/agent-server/adk_web_agents/canonical_debug/agent.py +29 -9
  5. package/template/apps/agent-server/adk_web_agents/services.py +25 -0
  6. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +40 -11
  7. package/template/apps/agent-server/src/southwind_agent_server/adk_debug_sessions.py +325 -0
  8. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +70 -6
  9. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +1 -1
  10. package/template/apps/agent-server/src/southwind_agent_server/app.py +16 -4
  11. package/template/apps/agent-server/src/southwind_agent_server/approval_resolution.py +30 -0
  12. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +9 -0
  13. package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +2 -1
  14. package/template/apps/agent-server/src/southwind_agent_server/engine.py +3 -1
  15. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +95 -130
  16. package/template/apps/agent-server/src/southwind_agent_server/openai_engine_validation.py +133 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +89 -20
  18. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_insert.py +34 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_queries.py +234 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_replay.py +44 -0
  21. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_status.py +83 -0
  22. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_store.py +331 -0
  23. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_summary.py +304 -0
  24. package/template/apps/agent-server/src/southwind_agent_server/report_lifecycle.py +217 -0
  25. package/template/apps/agent-server/src/southwind_agent_server/report_metrics.py +149 -0
  26. package/template/apps/agent-server/src/southwind_agent_server/report_presenter.py +280 -0
  27. package/template/apps/agent-server/src/southwind_agent_server/report_routes.py +126 -0
  28. package/template/apps/agent-server/src/southwind_agent_server/report_store.py +344 -0
  29. package/template/apps/agent-server/src/southwind_agent_server/report_types.py +111 -0
  30. package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +31 -2
  31. package/template/apps/agent-server/src/southwind_agent_server/service.py +56 -51
  32. package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +18 -1
  33. package/template/apps/agent-server/tests/test_adk_adapter.py +7 -9
  34. package/template/apps/agent-server/tests/test_adk_debug_sessions.py +215 -0
  35. package/template/apps/agent-server/tests/test_adk_web_canonical_debug.py +48 -1
  36. package/template/apps/agent-server/tests/test_openai_provider.py +58 -6
  37. package/template/apps/agent-server/tests/test_postgres_report_status.py +59 -0
  38. package/template/apps/agent-server/tests/test_postgres_report_summary.py +298 -0
  39. package/template/apps/agent-server/tests/test_provider_attempt_reporting.py +152 -0
  40. package/template/apps/agent-server/tests/test_report_attempt_lifecycle.py +126 -0
  41. package/template/apps/agent-server/tests/test_report_resilience.py +181 -0
  42. package/template/apps/agent-server/tests/test_report_store.py +286 -0
  43. package/template/apps/agent-server/tests/test_report_summary_parity.py +115 -0
  44. package/template/apps/agent-server/tests/test_report_terminal_sync.py +177 -0
  45. package/template/apps/server/src/agent/debug-session-facade-provider-failure.test.ts +269 -0
  46. package/template/apps/server/src/agent/debug-session-facade.test.ts +335 -0
  47. package/template/apps/server/src/agent/debug-session-facade.ts +302 -0
  48. package/template/apps/server/src/agent/debug-session-id.test.ts +37 -0
  49. package/template/apps/server/src/agent/debug-session-id.ts +37 -0
  50. package/template/apps/server/src/agent/debug-session-provider-reader.ts +54 -0
  51. package/template/apps/server/src/agent/debug-session-routes.test.ts +64 -0
  52. package/template/apps/server/src/agent/debug-session-routes.ts +73 -0
  53. package/template/apps/server/src/agent/debug-session-validation.ts +106 -0
  54. package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +2 -0
  55. package/template/apps/server/src/agent/provider-input-governance.test.ts +18 -0
  56. package/template/apps/server/src/agent/provider-input-governance.ts +37 -1
  57. package/template/apps/server/src/agent/remote-runtime.test.ts +7 -0
  58. package/template/apps/server/src/agent/remote-runtime.ts +1 -0
  59. package/template/apps/server/src/agent/run-authorization.test.ts +98 -0
  60. package/template/apps/server/src/agent/run-authorization.ts +32 -1
  61. package/template/apps/server/src/agent/run-binding-repository.ts +1 -0
  62. package/template/apps/server/src/agent/run-contracts.ts +5 -0
  63. package/template/apps/server/src/agent/run-facade-adk-debug.test.ts +39 -0
  64. package/template/apps/server/src/agent/run-facade.test.ts +78 -0
  65. package/template/apps/server/src/agent/run-facade.ts +33 -3
  66. package/template/apps/server/src/agent/run-input-commitment.ts +50 -0
  67. package/template/apps/server/src/agent/run-input-verification.ts +41 -0
  68. package/template/apps/server/src/agent/runtime-bootstrap-report.test.ts +136 -0
  69. package/template/apps/server/src/agent/runtime-bootstrap.ts +66 -1
  70. package/template/apps/server/src/agent/tool-host.test.ts +1 -0
  71. package/template/apps/server/src/agent/unavailable-routes.ts +4 -0
  72. package/template/apps/server/src/ai-runtime-report/actor-directory.ts +94 -0
  73. package/template/apps/server/src/ai-runtime-report/actor-handle.test.ts +27 -0
  74. package/template/apps/server/src/ai-runtime-report/actor-handle.ts +81 -0
  75. package/template/apps/server/src/ai-runtime-report/contracts.ts +91 -0
  76. package/template/apps/server/src/ai-runtime-report/remote-parser.ts +212 -0
  77. package/template/apps/server/src/ai-runtime-report/remote-port.ts +138 -0
  78. package/template/apps/server/src/ai-runtime-report/routes.ts +172 -0
  79. package/template/apps/server/src/ai-runtime-report/scope.ts +63 -0
  80. package/template/apps/server/src/ai-runtime-report/service.test.ts +271 -0
  81. package/template/apps/server/src/ai-runtime-report/service.ts +206 -0
  82. package/template/apps/server/src/license/runtime-audit-reliability.test.ts +2 -0
  83. package/template/apps/server/src/license/runtime-service.test.ts +6 -0
  84. package/template/apps/server/src/module-host/ai-operation-port.test.ts +63 -0
  85. package/template/apps/server/src/module-host/ai-operation-port.ts +30 -5
  86. package/template/apps/server/src/module-host/debug-session-provider-registration.test.ts +162 -0
  87. package/template/apps/server/src/module-host/debug-session-provider-registration.ts +84 -0
  88. package/template/apps/server/src/module-host/initialize-contracts.ts +7 -0
  89. package/template/apps/server/src/module-host/initialize.ts +6 -197
  90. package/template/apps/server/src/module-host/request-context.ts +1 -0
  91. package/template/apps/server/src/module-host/scoped-host.ts +214 -0
  92. package/template/apps/server/src/module-host.ts +1 -0
  93. package/template/apps/server/src/runtime-adk-debug.test.ts +25 -1
  94. package/template/apps/server/src/runtime-adk-debug.ts +8 -2
  95. package/template/apps/server/src/settings/drizzle-watermark-repository.integration.test.ts +4 -0
  96. package/template/apps/server/src/settings/drizzle-watermark-repository.ts +11 -3
  97. package/template/apps/server/src/settings/watermark-routes.test.ts +54 -1
  98. package/template/apps/server/src/settings/watermark-routes.ts +29 -1
  99. package/template/apps/server/src/system/operations.test.ts +54 -0
  100. package/template/apps/server/src/system/operations.ts +23 -1
  101. package/template/apps/server/src/system/seed.test.ts +4 -0
  102. package/template/apps/server/src/system/system-resource-monitoring.test.ts +82 -0
  103. package/template/apps/server/src/system/system-resource-monitoring.ts +300 -0
  104. package/template/apps/web/src/composables/useWatermarkSettings.webtest.ts +14 -0
  105. package/template/apps/web/src/composables/watermark-settings.ts +40 -11
  106. package/template/apps/web/src/composables/watermark-settings.webtest.ts +19 -6
  107. package/template/apps/web/src/layout/AppSidebar.vue +6 -1
  108. package/template/apps/web/src/layout/AppSidebar.webtest.ts +2 -0
  109. package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +2 -0
  110. package/template/apps/web/src/layout/GlobalWatermark.vue +4 -8
  111. package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +12 -5
  112. package/template/apps/web/src/pages/auth/LoginPage.vue +6 -1
  113. package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +20 -3
  114. package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +80 -0
  115. package/template/apps/web/src/pages/settings/WatermarkPreview.vue +72 -0
  116. package/template/apps/web/src/pages/settings/WatermarkPreview.webtest.ts +55 -0
  117. package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +80 -24
  118. package/template/apps/web/src/pages/settings/WatermarkSettingsSection.webtest.ts +115 -0
  119. package/template/apps/web/src/pages/system/AiRuntimePage.vue +129 -0
  120. package/template/apps/web/src/pages/system/SystemOperationsPage.vue +2 -1
  121. package/template/apps/web/src/pages/system/ai-runtime-presenter.ts +60 -0
  122. package/template/apps/web/src/pages/system/ai-runtime-presenter.webtest.ts +48 -0
  123. package/template/apps/web/src/pages/system/audit-time-range.ts +34 -2
  124. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +4 -3
  125. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +26 -1
  126. package/template/apps/web/src/pages/system/components/OperationGpuMonitor.vue +98 -0
  127. package/template/apps/web/src/pages/system/components/OperationGpuMonitor.webtest.ts +61 -0
  128. package/template/apps/web/src/pages/system/components/OperationServiceMonitor.vue +53 -0
  129. package/template/apps/web/src/pages/system/components/OperationServiceMonitor.webtest.ts +20 -1
  130. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +14 -1
  131. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeActivityChart.vue +85 -0
  132. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeCharts.webtest.ts +82 -0
  133. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +98 -0
  134. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +27 -0
  135. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue +168 -0
  136. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.vue +130 -0
  137. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.vue +82 -0
  138. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.webtest.ts +102 -0
  139. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeTokenByModelChart.vue +101 -0
  140. package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.ts +185 -0
  141. package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.webtest.ts +158 -0
  142. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +21 -7
  143. package/template/apps/web/src/router/error-navigation.ts +22 -0
  144. package/template/apps/web/src/router/index.ts +12 -23
  145. package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
  146. package/template/apps/web/src/services/ai-runtime-api.ts +164 -0
  147. package/template/apps/web/src/services/ai-runtime-api.webtest.ts +111 -0
  148. package/template/apps/web/src/services/operations-api.ts +28 -0
  149. package/template/docker-compose.agent-debug.yml +1 -1
  150. package/template/docs/architecture/ai-runtime.md +2 -1
  151. package/template/docs/architecture/module-host.md +3 -0
  152. package/template/docs/platform/agent-run-attestation.md +65 -0
  153. package/template/docs/platform/agent-runtime-observability.md +135 -0
  154. package/template/docs/platform/agent-runtime.md +47 -3
  155. package/template/docs/platform/operations-overview.md +13 -0
  156. package/template/docs/platform/platform-authentication.md +4 -3
  157. package/template/docs/platform/platform-shell-settings.md +15 -10
  158. package/template/docs/platform/web-system-crud.md +1 -1
  159. package/template/packages/config/package.json +1 -1
  160. package/template/packages/database/drizzle/0044_sweet_tarantula.sql +6 -0
  161. package/template/packages/database/drizzle/0045_tiny_cassandra_nova.sql +65 -0
  162. package/template/packages/database/drizzle/0046_tranquil_living_mummy.sql +6 -0
  163. package/template/packages/database/drizzle/0047_oval_luke_cage.sql +18 -0
  164. package/template/packages/database/drizzle/meta/0044_snapshot.json +8349 -0
  165. package/template/packages/database/drizzle/meta/0045_snapshot.json +8814 -0
  166. package/template/packages/database/drizzle/meta/0046_snapshot.json +8820 -0
  167. package/template/packages/database/drizzle/meta/0047_snapshot.json +8835 -0
  168. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  169. package/template/packages/database/package.json +1 -1
  170. package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
  171. package/template/packages/database/src/schema/agent-runtime.ts +114 -0
  172. package/template/packages/database/src/schema/platform-settings.ts +3 -0
  173. package/template/packages/jobs/package.json +1 -1
  174. package/template/packages/modules/package.json +1 -1
  175. package/template/packages/modules/src/system-admin-routes.ts +10 -0
  176. package/template/packages/modules/src/system-admin-ui-catalog.ts +1 -1
  177. package/template/packages/modules/src/system-api-control-plane.ts +17 -1
  178. package/template/packages/modules/src/system-audit-actions.ts +10 -0
  179. package/template/packages/modules/src/system-features.ts +8 -0
  180. package/template/packages/modules/src/system-module-catalog.test.ts +35 -0
  181. package/template/packages/modules/src/system-module-catalog.ts +15 -4
  182. package/template/packages/modules/src/system-modules-access.test.ts +69 -0
  183. package/template/packages/modules/src/system-modules.ts +13 -0
  184. package/template/packages/modules/src/system-permissions.ts +2 -0
  185. package/template/packages/server-sdk/index.ts +11 -0
  186. package/template/packages/server-sdk/package.json +2 -1
  187. package/template/packages/server-sdk/src/ai-operation-port.ts +5 -0
  188. package/template/packages/server-sdk/src/debug-session-provider.ts +63 -0
  189. package/template/packages/server-sdk/src/module-host.ts +5 -0
  190. package/template/packages/shared/index.ts +3 -0
  191. package/template/packages/shared/package.json +1 -1
  192. package/template/packages/shared/src/ai-runtime-report.ts +134 -0
  193. package/template/packages/shared/src/platform-settings.ts +17 -0
  194. package/template/scripts/lib/postgres-regression.ts +201 -0
@@ -0,0 +1,280 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from datetime import UTC, datetime
5
+ from typing import cast
6
+
7
+ from .report_types import (
8
+ AttemptReport,
9
+ CacheStatus,
10
+ ReportFilter,
11
+ RunReport,
12
+ iso,
13
+ )
14
+
15
+
16
+ def filtered_runs(values: Iterable[RunReport], query: ReportFilter) -> list[RunReport]:
17
+ return [
18
+ run
19
+ for run in scoped_runs(values, query)
20
+ if query.from_timestamp <= run.created_at <= query.to_timestamp
21
+ ]
22
+
23
+
24
+ def scoped_runs(values: Iterable[RunReport], query: ReportFilter) -> list[RunReport]:
25
+ return [
26
+ run
27
+ for run in values
28
+ if (query.selected_actor_id is None or run.actor_id == query.selected_actor_id)
29
+ and (
30
+ not query.scope_restricted
31
+ or run.actor_id in query.actor_ids
32
+ or run.department_id in query.department_ids
33
+ )
34
+ ]
35
+
36
+
37
+ def attempts_for(
38
+ values: Iterable[AttemptReport], runs: Iterable[RunReport]
39
+ ) -> list[AttemptReport]:
40
+ run_ids = {run.run_id for run in runs}
41
+ return sorted(
42
+ [item for item in values if item.run_id in run_ids],
43
+ key=lambda item: (item.run_id, item.sequence),
44
+ )
45
+
46
+
47
+ def filtered_attempts(
48
+ values: Iterable[AttemptReport],
49
+ runs: Iterable[RunReport],
50
+ query: ReportFilter,
51
+ ) -> list[AttemptReport]:
52
+ return [
53
+ attempt
54
+ for attempt in attempts_for(values, runs)
55
+ if attempt.finished_at is not None
56
+ and query.from_timestamp <= attempt.started_at <= query.to_timestamp
57
+ ]
58
+
59
+
60
+ def aggregate_summary(
61
+ runs: list[RunReport],
62
+ attempts: list[AttemptReport],
63
+ retention_days: int,
64
+ available_from: float | None,
65
+ generated_at: float,
66
+ ) -> dict[str, object]:
67
+ attempts = [item for item in attempts if item.finished_at is not None]
68
+ completed = count_status(runs, "completed")
69
+ failed = count_status(runs, "failed")
70
+ cancelled = count_status(runs, "cancelled")
71
+ denominator = completed + failed
72
+ hit = count_cache(attempts, "hit")
73
+ miss = count_cache(attempts, "miss")
74
+ unknown = count_cache(attempts, "unknown")
75
+ cache_pairs = [
76
+ item
77
+ for item in attempts
78
+ if item.usage.input_tokens is not None
79
+ and item.usage.cached_input_tokens is not None
80
+ ]
81
+ paired_input_tokens = sum_known(cache_pairs, "input_tokens")
82
+ paired_cached_tokens = sum_known(cache_pairs, "cached_input_tokens")
83
+ known_total_tokens = sum(item.usage.total_tokens is not None for item in attempts)
84
+ return {
85
+ "generatedAt": iso(generated_at),
86
+ "reportingAvailableFrom": iso(available_from),
87
+ "retentionDays": retention_days,
88
+ "runs": {
89
+ "total": len(runs),
90
+ "completed": completed,
91
+ "failed": failed,
92
+ "cancelled": cancelled,
93
+ "inProgress": len(runs) - completed - failed - cancelled,
94
+ "successRate": completed / denominator if denominator else None,
95
+ },
96
+ "cache": {
97
+ "hit": hit,
98
+ "miss": miss,
99
+ "unknown": unknown,
100
+ "hitRate": hit / (hit + miss) if hit + miss else None,
101
+ "tokenRate": (
102
+ paired_cached_tokens / paired_input_tokens
103
+ if paired_input_tokens > 0
104
+ else None
105
+ ),
106
+ "coverageRate": (hit + miss) / len(attempts) if attempts else 0,
107
+ },
108
+ "tokens": {
109
+ "input": sum_reported(attempts, "input_tokens"),
110
+ "output": sum_reported(attempts, "output_tokens"),
111
+ "cachedInput": sum_reported(attempts, "cached_input_tokens"),
112
+ "reasoning": sum_reported(attempts, "reasoning_tokens"),
113
+ "total": sum_reported(attempts, "total_tokens"),
114
+ "coverageRate": (known_total_tokens / len(attempts) if attempts else 0),
115
+ },
116
+ "byModel": by_model(attempts),
117
+ "activity": activity(runs, attempts),
118
+ }
119
+
120
+
121
+ def count_status(runs: list[RunReport], status: str) -> int:
122
+ return sum(run.status == status for run in runs)
123
+
124
+
125
+ def count_cache(attempts: list[AttemptReport], status: CacheStatus) -> int:
126
+ return sum(item.usage.cache_status == status for item in attempts)
127
+
128
+
129
+ def sum_known(attempts: list[AttemptReport], field: str) -> int:
130
+ return sum(
131
+ value for item in attempts if (value := getattr(item.usage, field)) is not None
132
+ )
133
+
134
+
135
+ def sum_reported(attempts: list[AttemptReport], field: str) -> int | None:
136
+ values = [
137
+ cast(int, value)
138
+ for item in attempts
139
+ if (value := getattr(item.usage, field)) is not None
140
+ ]
141
+ if values:
142
+ return sum(values)
143
+ return 0 if not attempts else None
144
+
145
+
146
+ def by_model(attempts: list[AttemptReport]) -> list[dict[str, object]]:
147
+ groups: dict[tuple[str, str], list[AttemptReport]] = {}
148
+ for item in attempts:
149
+ model = item.reported_model or item.requested_model
150
+ groups.setdefault((item.deployment_key, model), []).append(item)
151
+ return [
152
+ {
153
+ "deploymentKey": deployment,
154
+ "model": model,
155
+ "attempts": len(items),
156
+ "inputTokens": sum_reported(items, "input_tokens"),
157
+ "outputTokens": sum_reported(items, "output_tokens"),
158
+ "cachedInputTokens": sum_reported(items, "cached_input_tokens"),
159
+ "reasoningTokens": sum_reported(items, "reasoning_tokens"),
160
+ "totalTokens": sum_reported(items, "total_tokens"),
161
+ "unknownUsageCount": sum(item.usage.source == "unknown" for item in items),
162
+ }
163
+ for (deployment, model), items in sorted(groups.items())
164
+ ]
165
+
166
+
167
+ def activity(
168
+ runs: list[RunReport], attempts: list[AttemptReport]
169
+ ) -> list[dict[str, object]]:
170
+ result: dict[str, dict[str, int]] = {}
171
+ for run in runs:
172
+ day = datetime.fromtimestamp(run.created_at, tz=UTC).date().isoformat()
173
+ row = result.setdefault(
174
+ day,
175
+ {
176
+ "runs": 0,
177
+ "completed": 0,
178
+ "failed": 0,
179
+ "totalTokens": 0,
180
+ "tokenAttempts": 0,
181
+ "knownTokenAttempts": 0,
182
+ },
183
+ )
184
+ row["runs"] += 1
185
+ row["completed"] += int(run.status == "completed")
186
+ row["failed"] += int(run.status == "failed")
187
+ for attempt in attempts:
188
+ day = datetime.fromtimestamp(attempt.started_at, tz=UTC).date().isoformat()
189
+ row = result.setdefault(
190
+ day,
191
+ {
192
+ "runs": 0,
193
+ "completed": 0,
194
+ "failed": 0,
195
+ "totalTokens": 0,
196
+ "tokenAttempts": 0,
197
+ "knownTokenAttempts": 0,
198
+ },
199
+ )
200
+ row["tokenAttempts"] += 1
201
+ if attempt.usage.total_tokens is not None:
202
+ row["totalTokens"] += attempt.usage.total_tokens
203
+ row["knownTokenAttempts"] += 1
204
+ return [
205
+ {
206
+ "date": day,
207
+ "runs": result[day]["runs"],
208
+ "completed": result[day]["completed"],
209
+ "failed": result[day]["failed"],
210
+ "totalTokens": (
211
+ result[day]["totalTokens"]
212
+ if result[day]["tokenAttempts"]
213
+ and result[day]["knownTokenAttempts"] == result[day]["tokenAttempts"]
214
+ else None
215
+ ),
216
+ }
217
+ for day in sorted(result)
218
+ ]
219
+
220
+
221
+ def run_value(run: RunReport, attempts: list[AttemptReport]) -> dict[str, object]:
222
+ models = sorted({item.reported_model or item.requested_model for item in attempts})
223
+ cache = (
224
+ "hit"
225
+ if any(item.usage.cache_status == "hit" for item in attempts)
226
+ else "miss"
227
+ if any(item.usage.cache_status == "miss" for item in attempts)
228
+ else "unknown"
229
+ )
230
+ return {
231
+ "runId": run.run_id,
232
+ "operationKey": run.operation_key,
233
+ "actorId": run.actor_id,
234
+ "departmentId": run.department_id,
235
+ "status": run.status,
236
+ "startedAt": iso(run.started_at),
237
+ "finishedAt": iso(run.finished_at),
238
+ "durationMs": run.duration_ms,
239
+ "attemptCount": len(attempts),
240
+ "models": models,
241
+ "cacheStatus": cache,
242
+ "usage": usage_value(attempts),
243
+ }
244
+
245
+
246
+ def usage_value(attempts: list[AttemptReport]) -> dict[str, int | None]:
247
+ def total(field: str) -> int | None:
248
+ values = [
249
+ value
250
+ for item in attempts
251
+ if (value := getattr(item.usage, field)) is not None
252
+ ]
253
+ return sum(values) if values else None
254
+
255
+ return {
256
+ "inputTokens": total("input_tokens"),
257
+ "outputTokens": total("output_tokens"),
258
+ "cachedInputTokens": total("cached_input_tokens"),
259
+ "reasoningTokens": total("reasoning_tokens"),
260
+ "totalTokens": total("total_tokens"),
261
+ }
262
+
263
+
264
+ def attempt_value(item: AttemptReport) -> dict[str, object]:
265
+ return {
266
+ "attemptId": item.attempt_id,
267
+ "sequence": item.sequence,
268
+ "toolRound": item.tool_round,
269
+ "deploymentKey": item.deployment_key,
270
+ "model": item.reported_model or item.requested_model,
271
+ "outcome": item.outcome,
272
+ "cacheStatus": item.usage.cache_status,
273
+ "usageSource": item.usage.source,
274
+ **usage_value([item]),
275
+ "firstTokenLatencyMs": item.usage.first_token_latency_ms,
276
+ "durationMs": item.duration_ms,
277
+ "safeErrorCode": item.safe_error_code,
278
+ "startedAt": iso(item.started_at),
279
+ "finishedAt": iso(item.finished_at),
280
+ }
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Annotated, Literal
5
+
6
+ from fastapi import APIRouter, Header, Query
7
+ from fastapi.responses import JSONResponse, PlainTextResponse
8
+
9
+ from .http_helpers import authorized, safe_error
10
+ from .report_store import AgentRuntimeReportStore
11
+ from .report_types import ReportFilter
12
+
13
+ type ReportRange = Literal["day", "month", "retained"]
14
+ type ScopeMode = Literal["all", "bounded", "none"]
15
+
16
+
17
+ def create_report_router(
18
+ store: AgentRuntimeReportStore,
19
+ internal_token: str | None,
20
+ ) -> APIRouter:
21
+ router = APIRouter()
22
+
23
+ @router.get("/metrics", response_model=None)
24
+ async def metrics() -> PlainTextResponse:
25
+ return PlainTextResponse(
26
+ store.metrics.render(),
27
+ media_type="text/plain; version=0.0.4; charset=utf-8",
28
+ headers={"Cache-Control": "no-store"},
29
+ )
30
+
31
+ @router.get("/api/internal/ai-runtime-report/summary", response_model=None)
32
+ async def report_summary(
33
+ range_name: Annotated[ReportRange, Query(alias="range")] = "day",
34
+ actor_id: Annotated[str | None, Query(alias="actorId", max_length=64)] = None,
35
+ scope_actor_ids: Annotated[
36
+ list[str] | None, Query(alias="scopeActorId", max_length=64)
37
+ ] = None,
38
+ scope_department_ids: Annotated[
39
+ list[str] | None, Query(alias="scopeDepartmentId", max_length=64)
40
+ ] = None,
41
+ scope_mode: Annotated[ScopeMode, Query(alias="scopeMode")] = "none",
42
+ authorization: Annotated[str | None, Header()] = None,
43
+ ) -> dict[str, object] | JSONResponse:
44
+ if not authorized(authorization, internal_token):
45
+ return safe_error(403, "FORBIDDEN", "内部调用认证失败", False)
46
+ return await store.summary(
47
+ report_filter(
48
+ store,
49
+ range_name,
50
+ actor_id,
51
+ scope_mode,
52
+ scope_actor_ids,
53
+ scope_department_ids,
54
+ )
55
+ )
56
+
57
+ @router.get("/api/internal/ai-runtime-report/runs", response_model=None)
58
+ async def report_runs(
59
+ range_name: Annotated[ReportRange, Query(alias="range")] = "day",
60
+ actor_id: Annotated[str | None, Query(alias="actorId", max_length=64)] = None,
61
+ scope_actor_ids: Annotated[
62
+ list[str] | None, Query(alias="scopeActorId", max_length=64)
63
+ ] = None,
64
+ scope_department_ids: Annotated[
65
+ list[str] | None, Query(alias="scopeDepartmentId", max_length=64)
66
+ ] = None,
67
+ scope_mode: Annotated[ScopeMode, Query(alias="scopeMode")] = "none",
68
+ page: Annotated[int, Query(ge=1)] = 1,
69
+ page_size: Annotated[int, Query(alias="pageSize", ge=1, le=100)] = 20,
70
+ authorization: Annotated[str | None, Header()] = None,
71
+ ) -> dict[str, object] | JSONResponse:
72
+ if not authorized(authorization, internal_token):
73
+ return safe_error(403, "FORBIDDEN", "内部调用认证失败", False)
74
+ return await store.list_runs(
75
+ report_filter(
76
+ store,
77
+ range_name,
78
+ actor_id,
79
+ scope_mode,
80
+ scope_actor_ids,
81
+ scope_department_ids,
82
+ ),
83
+ page,
84
+ page_size,
85
+ )
86
+
87
+ @router.get(
88
+ "/api/internal/ai-runtime-report/runs/{run_id}",
89
+ response_model=None,
90
+ )
91
+ async def report_run(
92
+ run_id: str,
93
+ authorization: Annotated[str | None, Header()] = None,
94
+ ) -> dict[str, object] | JSONResponse:
95
+ if not authorized(authorization, internal_token):
96
+ return safe_error(403, "FORBIDDEN", "内部调用认证失败", False)
97
+ value = await store.get_run(run_id)
98
+ if value is None:
99
+ return safe_error(404, "RUN_CONFLICT", "Agent Run 不存在", False)
100
+ return value
101
+
102
+ return router
103
+
104
+
105
+ def report_filter(
106
+ store: AgentRuntimeReportStore,
107
+ range_name: ReportRange,
108
+ actor_id: str | None,
109
+ scope_mode: ScopeMode,
110
+ scope_actor_ids: list[str] | None,
111
+ scope_department_ids: list[str] | None,
112
+ ) -> ReportFilter:
113
+ now = time.time()
114
+ days = {
115
+ "day": 1,
116
+ "month": 30,
117
+ "retained": store.retention_days,
118
+ }[range_name]
119
+ return ReportFilter(
120
+ from_timestamp=now - days * 24 * 60 * 60,
121
+ to_timestamp=now,
122
+ selected_actor_id=actor_id,
123
+ scope_restricted=scope_mode != "all",
124
+ actor_ids=tuple(dict.fromkeys(scope_actor_ids or ())),
125
+ department_ids=tuple(dict.fromkeys(scope_department_ids or ())),
126
+ )