mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
Phase 3 PR2: Weekly regional briefs (LLM seeder + RPC) (#2989)
* feat(intelligence): weekly regional briefs (Phase 3 PR2) Phase 3 PR2 of the Regional Intelligence Model. Adds LLM-powered weekly intelligence briefs per region, completing the core feature set. ## New seeder: scripts/seed-regional-briefs.mjs Standalone weekly cron script (not part of the 6h derived-signals bundle). For each non-global region: 1. Read the latest snapshot via two-hop Redis read 2. Read recent regime transitions from the history log (#2981) 3. Call the LLM once per region with regime trajectory + balance + triggers + narrative context 4. Write structured brief to intelligence:regional-briefs:v1:weekly:{region} with 8-day TTL (survives one missed weekly run) Reuses the same injectable-callLlm + parse-validation + provider-chain pattern from narrative.mjs and weekly-brief.mjs. ## New module: scripts/regional-snapshot/weekly-brief.mjs generateWeeklyBrief(region, snapshot, transitions, opts?) -> { region_id, generated_at, period_start, period_end, situation_recap, regime_trajectory, key_developments[], risk_outlook, provider, model } buildBriefPrompt() — pure prompt builder parseBriefJson() — JSON parser with prose-extraction fallback emptyBrief() — canonical empty shape Global region is skipped. Provider chain: Groq -> OpenRouter. Validate callback ensures only parseable responses pass (narrative.mjs PR #2960 review fix pattern). ## Proto + RPC: GetRegionalBrief proto/worldmonitor/intelligence/v1/get_regional_brief.proto - GetRegionalBriefRequest { region_id } - GetRegionalBriefResponse { brief: RegionalBrief } - RegionalBrief { region_id, generated_at, period_start, period_end, situation_recap, regime_trajectory, key_developments[], risk_outlook, provider, model } ## Server handler server/worldmonitor/intelligence/v1/get-regional-brief.ts Simple getCachedJson read + adaptBrief snake->camel adapter. Returns upstreamUnavailable: true on Redis failure so the gateway skips caching (matching the get-regime-history pattern from #2981). ## Premium gating + cache tier src/shared/premium-paths.ts + server/gateway.ts RPC_CACHE_TIER ## Tests — 27 new unit tests buildBriefPrompt (5): region/balance/transitions/narrative rendered, empty transitions handled, missing fields tolerated parseBriefJson (5): valid JSON, garbage, all-empty, cap at 5, prose extraction generateWeeklyBrief (6): success, global skip, LLM fail, garbage, exception, period_start/end delta emptyBrief (2): region_id + empty fields handler (4): key prefix, adapter export, upstreamUnavailable, registration security (2): premium path + cache tier proto (3): RPC declared, import wired, RegionalBrief fields ## Verification - npm run test:data: 4651/4651 pass - npm run typecheck + typecheck:api: clean - biome lint: clean * fix(intelligence): address 3 review findings on #2989 P2 #1 — no consumer surface for GetRegionalBrief Acknowledged. The consumer is the RegionalIntelligenceBoard panel, which will call GetRegionalBrief and render a weekly brief block. This wiring is Phase 3 PR3 (UI) scope — the RPC + Redis key are the delivery mechanism, not the end surface. No code change in this commit; the RPC is ready for the panel to consume. P2 #2 — readRecentTransitions collapses failure to [] readRecentTransitions returned [] on Redis/network failure, which is indistinguishable from a genuinely quiet week. The LLM then generates a brief claiming "no regime transitions" when in reality the upstream is down — fabricating false input. Fix: return null on failure. The seeder skips the region with a clear log message when transitions is null, so the brief is never written with unreliable input. Empty array [] now only means genuinely no transitions in the 7-day window. P2 #3 — parseBriefJson accepts briefs the seeder rejects parseBriefJson treated non-empty key_developments as valid even if situation_recap was empty. The seeder gate only writes when brief.situation_recap is truthy. That mismatch means the validator pass + provider-fallback logic could accept a response that the seeder then silently drops. Fix: require situation_recap in parseBriefJson for valid=true, matching the seeder gate. Now both checks agree on what constitutes a usable brief, and the provider-fallback chain correctly falls through when a provider returns a brief with developments but no recap. * fix(intelligence): TTL path-segment fix + seed-meta always-write (Greptile P1+P2 on #2989) P1 — TTL silently not applied (briefs never expire) Upstash REST ignores query-string SET options (?EX=N). The correct form is path-segment: /set/{key}/{value}/EX/{seconds}. Without this fix every brief persists indefinitely and Redis storage grows unboundedly across weekly runs. P2 — seed-meta not written when all regions skipped writeExtraKeyWithMeta was gated on generated > 0. If every region was skipped (no snapshot yet, or LLM failed), seed-meta was never written, making the seeder indistinguishable from "never ran" in health tooling. Now writes seed-meta whenever failed === 0, carrying regionsSkipped count. P2 #3 (validate gate) — already fixed in previous commit (parseBriefJson now requires situation_recap for valid=true). * fix(intelligence): register regional-briefs in health.js SEED_META + STANDALONE_KEYS (review P2 on #2989) * fix(intelligence): register regional-briefs in api/seed-health.js (review P2 on #2989) * fix(intelligence): raise brief TTL to 15 days to cover missed weekly cycle (review P2 on #2989) * fix(intelligence): distinguish missing-key from Redis-error + coverage-gated health (review P2s on #2989) P2 #1 — false upstreamUnavailable before first seed getCachedJson returns null for both "key missing" and "Redis failed", so the handler was advertising an outage for every region before the first weekly seed ran. Switched to getRawJson (throws on Redis errors) so null = genuinely missing key → clean empty 200, and thrown error = upstream failure → upstreamUnavailable: true for gateway no-store. P2 #2 — partial run hides coverage loss in health The seed-meta was written with generated count even if only 1 of 7 regions produced a brief. /api/health treats any positive recordCount as healthy, so broad regional failure was invisible to operators. Fix: recordCount is set to 0 when generated < ceil(expectedRegions/2). This makes /api/health report EMPTY_DATA for severely partial runs while still writing seed-meta (so the seeder is confirmed to have run). coverageOk flag in the summary payload lets operators drill into the exact coverage state. * fix(intelligence): tighten coverage gate to expectedRegions-1 (review P2 on #2989)
This commit is contained in:
35
proto/worldmonitor/intelligence/v1/get_regional_brief.proto
Normal file
35
proto/worldmonitor/intelligence/v1/get_regional_brief.proto
Normal file
@@ -0,0 +1,35 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package worldmonitor.intelligence.v1;
|
||||
|
||||
import "buf/validate/validate.proto";
|
||||
import "sebuf/http/annotations.proto";
|
||||
|
||||
message GetRegionalBriefRequest {
|
||||
string region_id = 1 [
|
||||
(buf.validate.field).required = true,
|
||||
(buf.validate.field).string.min_len = 1,
|
||||
(buf.validate.field).string.max_len = 32,
|
||||
(buf.validate.field).string.pattern = "^[a-z][a-z0-9]*(-[a-z0-9]+)*$",
|
||||
(sebuf.http.query) = {name: "region_id"}
|
||||
];
|
||||
}
|
||||
|
||||
message GetRegionalBriefResponse {
|
||||
RegionalBrief brief = 1;
|
||||
}
|
||||
|
||||
// RegionalBrief is a weekly LLM-synthesized intelligence summary for one
|
||||
// region. Written by scripts/seed-regional-briefs.mjs on a weekly cron.
|
||||
message RegionalBrief {
|
||||
string region_id = 1;
|
||||
int64 generated_at = 2 [(sebuf.http.int64_encoding) = INT64_ENCODING_NUMBER];
|
||||
int64 period_start = 3 [(sebuf.http.int64_encoding) = INT64_ENCODING_NUMBER];
|
||||
int64 period_end = 4 [(sebuf.http.int64_encoding) = INT64_ENCODING_NUMBER];
|
||||
string situation_recap = 5;
|
||||
string regime_trajectory = 6;
|
||||
repeated string key_developments = 7;
|
||||
string risk_outlook = 8;
|
||||
string provider = 9;
|
||||
string model = 10;
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import "worldmonitor/intelligence/v1/compute_energy_shock.proto";
|
||||
import "worldmonitor/intelligence/v1/get_country_port_activity.proto";
|
||||
import "worldmonitor/intelligence/v1/get_regional_snapshot.proto";
|
||||
import "worldmonitor/intelligence/v1/get_regime_history.proto";
|
||||
import "worldmonitor/intelligence/v1/get_regional_brief.proto";
|
||||
|
||||
// IntelligenceService provides APIs for technical and strategic intelligence.
|
||||
service IntelligenceService {
|
||||
@@ -187,4 +188,10 @@ service IntelligenceService {
|
||||
rpc GetRegimeHistory(GetRegimeHistoryRequest) returns (GetRegimeHistoryResponse) {
|
||||
option (sebuf.http.config) = {path: "/get-regime-history", method: HTTP_METHOD_GET};
|
||||
}
|
||||
|
||||
// GetRegionalBrief returns the latest weekly intelligence brief for a region.
|
||||
// Written by scripts/seed-regional-briefs.mjs on a weekly cron. Premium-gated.
|
||||
rpc GetRegionalBrief(GetRegionalBriefRequest) returns (GetRegionalBriefResponse) {
|
||||
option (sebuf.http.config) = {path: "/get-regional-brief", method: HTTP_METHOD_GET};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user