create-windy 0.4.1 → 0.5.1

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 (174) hide show
  1. package/dist/cli.js +292 -125
  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/system/operations.test.ts +54 -0
  96. package/template/apps/server/src/system/operations.ts +23 -1
  97. package/template/apps/server/src/system/seed.test.ts +4 -0
  98. package/template/apps/server/src/system/system-resource-monitoring.test.ts +82 -0
  99. package/template/apps/server/src/system/system-resource-monitoring.ts +300 -0
  100. package/template/apps/web/src/layout/AppSidebar.vue +6 -1
  101. package/template/apps/web/src/layout/AppSidebar.webtest.ts +2 -0
  102. package/template/apps/web/src/pages/auth/LoginPage.vue +6 -1
  103. package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +20 -3
  104. package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +80 -0
  105. package/template/apps/web/src/pages/system/AiRuntimePage.vue +129 -0
  106. package/template/apps/web/src/pages/system/SystemOperationsPage.vue +2 -1
  107. package/template/apps/web/src/pages/system/ai-runtime-presenter.ts +60 -0
  108. package/template/apps/web/src/pages/system/ai-runtime-presenter.webtest.ts +48 -0
  109. package/template/apps/web/src/pages/system/audit-time-range.ts +34 -2
  110. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +4 -3
  111. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +26 -1
  112. package/template/apps/web/src/pages/system/components/OperationGpuMonitor.vue +98 -0
  113. package/template/apps/web/src/pages/system/components/OperationGpuMonitor.webtest.ts +61 -0
  114. package/template/apps/web/src/pages/system/components/OperationServiceMonitor.vue +53 -0
  115. package/template/apps/web/src/pages/system/components/OperationServiceMonitor.webtest.ts +20 -1
  116. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +14 -1
  117. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeActivityChart.vue +85 -0
  118. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeCharts.webtest.ts +82 -0
  119. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +98 -0
  120. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +27 -0
  121. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue +168 -0
  122. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.vue +130 -0
  123. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.vue +82 -0
  124. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.webtest.ts +102 -0
  125. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeTokenByModelChart.vue +101 -0
  126. package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.ts +185 -0
  127. package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.webtest.ts +158 -0
  128. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +21 -7
  129. package/template/apps/web/src/router/error-navigation.ts +22 -0
  130. package/template/apps/web/src/router/index.ts +12 -23
  131. package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
  132. package/template/apps/web/src/services/ai-runtime-api.ts +164 -0
  133. package/template/apps/web/src/services/ai-runtime-api.webtest.ts +111 -0
  134. package/template/apps/web/src/services/operations-api.ts +28 -0
  135. package/template/docker-compose.agent-debug.yml +1 -1
  136. package/template/docs/architecture/ai-runtime.md +2 -1
  137. package/template/docs/architecture/module-host.md +3 -0
  138. package/template/docs/platform/agent-run-attestation.md +65 -0
  139. package/template/docs/platform/agent-runtime-observability.md +135 -0
  140. package/template/docs/platform/agent-runtime.md +47 -3
  141. package/template/docs/platform/operations-overview.md +13 -0
  142. package/template/docs/platform/platform-authentication.md +4 -3
  143. package/template/packages/config/package.json +1 -1
  144. package/template/packages/database/drizzle/0045_tiny_cassandra_nova.sql +65 -0
  145. package/template/packages/database/drizzle/0046_tranquil_living_mummy.sql +6 -0
  146. package/template/packages/database/drizzle/0047_oval_luke_cage.sql +18 -0
  147. package/template/packages/database/drizzle/meta/0045_snapshot.json +8814 -0
  148. package/template/packages/database/drizzle/meta/0046_snapshot.json +8820 -0
  149. package/template/packages/database/drizzle/meta/0047_snapshot.json +8835 -0
  150. package/template/packages/database/drizzle/meta/_journal.json +21 -0
  151. package/template/packages/database/package.json +1 -1
  152. package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
  153. package/template/packages/database/src/schema/agent-runtime.ts +114 -0
  154. package/template/packages/jobs/package.json +1 -1
  155. package/template/packages/modules/package.json +1 -1
  156. package/template/packages/modules/src/system-admin-routes.ts +10 -0
  157. package/template/packages/modules/src/system-admin-ui-catalog.ts +1 -1
  158. package/template/packages/modules/src/system-api-control-plane.ts +17 -1
  159. package/template/packages/modules/src/system-audit-actions.ts +10 -0
  160. package/template/packages/modules/src/system-features.ts +8 -0
  161. package/template/packages/modules/src/system-module-catalog.test.ts +35 -0
  162. package/template/packages/modules/src/system-module-catalog.ts +15 -4
  163. package/template/packages/modules/src/system-modules-access.test.ts +69 -0
  164. package/template/packages/modules/src/system-modules.ts +13 -0
  165. package/template/packages/modules/src/system-permissions.ts +2 -0
  166. package/template/packages/server-sdk/index.ts +11 -0
  167. package/template/packages/server-sdk/package.json +2 -1
  168. package/template/packages/server-sdk/src/ai-operation-port.ts +5 -0
  169. package/template/packages/server-sdk/src/debug-session-provider.ts +63 -0
  170. package/template/packages/server-sdk/src/module-host.ts +5 -0
  171. package/template/packages/shared/index.ts +3 -0
  172. package/template/packages/shared/package.json +1 -1
  173. package/template/packages/shared/src/ai-runtime-report.ts +134 -0
  174. 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
+ )