Oracle Canvas Component Schema and Qwen 3.6 integration #31

Merged
sagnik merged 1 commits from feat/#30 into main 2026-04-20 01:43:42 +05:30
129 changed files with 625213 additions and 262 deletions

View File

@@ -0,0 +1,209 @@
# Oracle Canvas Codebook Production Truth
Date: 2026-04-19
Repo: `Project_Velocity`
## Purpose
This document freezes the current production truth for the Oracle Canvas template/codebook system, the expanded GPT and Claude corpora, the runtime merge policy, and the current rendering limits that matter for delivery.
This is not a concept note. It is the implementation-facing truth for the Oracle template layer as it exists now.
## Current Source Of Truth
The Oracle template book is split across three layers:
1. Structural database schema
- `backend/oracle/schema_extension_v2.sql`
- Defines:
- `oracle_template_chapters`
- `oracle_template_subchapters`
- `oracle_template_seed_examples`
- chapter/subchapter linkage on `oracle_component_templates`
- `oracle_synthetic_generation_jobs`
2. Runtime seed DB
- `backend/oracle/oracle_template_seed_db.json`
- This is the lightweight fallback DB shipped with the runtime.
- It is structurally correct but incomplete relative to the intended corpus.
3. Expanded authoring corpora
- GPT pack:
- `Project_Velocity/.Agent Context/Sprint 1/Sayan Multi-Surface and Oracle Delivery Pack/Sample JSON Schema/GPT 5.4/oracle_canvas_json_expansion_pack/db/oracle_template_seed_db_expanded_v1.pretty.json`
- Claude pack:
- `Project_Velocity/.Agent Context/Sprint 1/Sayan Multi-Surface and Oracle Delivery Pack/Sample JSON Schema/Claude Sonnet 4.6/oracle_template_expansion/oracle_template_seed_db_expanded.json`
4. Frozen runtime merge artifact
- `backend/oracle/oracle_runtime_codebook_merged.json`
- This is the deploy-safe merged corpus generated from the GPT and Claude packs.
- Production should prefer this file over the authoring packs whenever it is present.
## Corpus Status
The expanded corpora are materially useful and production-relevant.
### GPT 5.4 pack
- Chapters: `6`
- Subchapters: `24`
- Seed examples: `1200`
- Shape: already close to runtime needs
- Key field for examples: `seed_examples`
### Claude Sonnet 4.6 pack
- Chapters: `6`
- Subchapters: `24`
- Examples: `1200`
- Key field for examples: `examples`
- Shape: close, but requires normalization into runtime form
### Runtime fallback pack
- Chapters: `6`
- Subchapters: `24`
- Seed examples declared in metadata: `36`
- Seed examples physically present: lower than metadata
- Useful only as a fallback, not as the primary production corpus
## Super Codebook Policy
The current runtime now treats the codebook as a merged corpus rather than a single-file static DB.
The merge policy is:
1. Load GPT pack first.
2. Load Claude pack second.
3. Load runtime fallback pack last.
4. Normalize all example records to one runtime contract.
5. Deduplicate by:
- `subchapter_id`
- `template_name`
- `title`
6. Prefer in this order:
- GPT 5.4 examples
- canonical examples
- fallback records only when no richer example exists
This behavior is implemented in:
- `backend/oracle/codebook_service.py`
- `backend/scripts/build_oracle_runtime_codebook.py`
That file is now the effective runtime “super codebook” layer.
The generated runtime artifact currently contains the merged deployable corpus and is suitable for Linux-box deployment without requiring `.Agent Context` lookups at request time.
## What The Runtime Actually Uses
The runtime no longer needs to rely on hardcoded template lists in the Oracle v1 router.
The codebook service now provides:
- merged corpus loading
- search over both corpora
- normalized template listing
- best-match template synthesis from a user prompt
Primary runtime functions:
- `codebook_service.stats()`
- `codebook_service.list_templates(...)`
- `codebook_service.search_examples(prompt, limit=...)`
- `codebook_service.synthesize_template(prompt, data_shapes=...)`
## Current Supported Runtime Output Families
The expanded corpora include more component types than the current frontend renderer supports directly.
The current production-safe strategy is:
1. keep the full codebook corpus
2. map high-variety codebook component families into a smaller supported runtime renderer set
3. let Oracle render reliably today instead of failing on unsupported component types
### Supported runtime renderers today
- `textCanvas`
- `kpiTile`
- `barChart`
- `lineChart`
- `geoMap`
- `table`
- `pipelineBoard`
- `timeline`
- `activityStream`
- `errorNotice`
### Codebook-to-runtime normalization policy
Examples:
- `summary_card`, `summary_strip`, `metric_card_group`, `gauge_stack`
- mapped to `kpiTile`
- `lead_profile_card`, `property_card`, `data_table`, `leaderboard_table`, `matrix_grid`
- mapped to `table`
- `interaction_timeline`, `message_thread_summary`
- mapped to `activityStream`
- `heatmap`
- mapped to `geoMap`
This is deliberate. It keeps the UI stable while preserving the larger design vocabulary inside the template book.
## What Is Production-Ready Now
- Oracle template DB schema exists.
- Oracle template taxonomy APIs exist.
- Expanded GPT and Claude corpora are available locally in the repo.
- Runtime codebook merge and retrieval is implemented in `codebook_service.py`.
- A frozen merged runtime codebook now exists at `backend/oracle/oracle_runtime_codebook_merged.json`.
- Oracle v1 template listing/synthesis is being moved to the codebook-backed path.
- Oracle backend can now emit `textCanvas` planning blocks and the frontend has a renderer for them.
## What Is Still Constrained
- The runtime is not yet rendering all 47+ component families natively.
- The current system uses safe projection into supported runtime renderers.
- The template taxonomy routes existed, but were incorrectly using `user.role` as `tenant_id`; that has been corrected toward a fixed Oracle tenant policy.
- The lightweight fallback JSON DB remains incomplete and should not be treated as the main corpus.
## What Nemoclaw / Oracle Should Use For Retrieval
The correct order for Oracle prompt handling is:
1. Parse prompt.
2. Retrieve matching codebook examples from the merged corpus.
3. Build a safe retrieval plan against allowed DB datasets.
4. Query live CRM/intelligence/inventory datasets.
5. Build Oracle Canvas JSON with supported runtime component types.
6. Append to the existing canvas.
The codebook is not the final UI payload by itself.
It is the reference layer that guides:
- component family selection
- chapter/subchapter intent
- layout direction
- data-shape expectations
- policy hints
- backend contract hints
## Recommended Near-Term Hardening
1. Materialize a generated runtime codebook file if Linux deployment should not depend on `.Agent Context`.
2. Add explicit metadata versioning to the merged corpus.
3. Add a small admin endpoint for codebook stats and source summary.
4. Expand renderer coverage incrementally rather than trying to support all component families at once.
5. Add a batch offline export path if the team wants a frozen deploy artifact.
## Operator Bottom Line
The Oracle “book with chapters and JSON schema examples” is real and already useful.
The correct production interpretation is:
- DB schema and APIs are already present
- GPT and Claude expansion packs are the real high-value corpus
- `backend/oracle/codebook_service.py` is the runtime super-codebook layer
- Oracle should retrieve from this merged corpus first, then query live DB data, then render supported JSON Canvas components

View File

@@ -0,0 +1,382 @@
# Oracle Canvas Runtime and Ollama Batch Architecture
Date: 2026-04-19
Repo: `Project_Velocity`
## Purpose
This document defines the current production Oracle Canvas runtime path, the intended Ollama/Nemoclaw model-routing strategy, and the target batch-processing API shape the team can use if Velocity exposes Oracle or coding-agent capabilities through the local model stack.
This is the operator and engineering artifact. It exists to remove ambiguity.
## Runtime Topology
### Linux origin box
Role:
- hosts Velocity frontend
- hosts FastAPI backend
- hosts PostgreSQL and application services
- terminates app-origin requests under the public site path
Primary concern:
- application routing
- auth/session enforcement
- Oracle API execution
- CRM/intelligence/inventory data access
### GPU box
Role:
- hosts ComfyUI
- hosts heavy model runtime
- hosts Ollama / Nemoclaw execution plane
- stores runtime/model payloads on NVMe only
Primary concern:
- inference
- media generation
- model serving
- agent runtime workloads
### Ingress
Role:
- stable public entry for GPU-backed services
- hides raw GPU host details from application code
Non-negotiable rule:
- never wire Oracle or frontend code to a raw GPU public IP
## Oracle Canvas Current Execution Path
The production-safe Oracle path is now:
1. User submits prompt from Oracle Canvas frontend.
2. Frontend calls:
- `/api/oracle/v1/canvas-pages/{page_id}/prompts`
3. FastAPI Oracle orchestrator:
- loads user context
- retrieves best codebook matches
- builds a safe retrieval plan
- queries approved datasets from PostgreSQL
- produces JSON Canvas components
- commits a page revision
4. Frontend reloads/reconciles the canvas state and renders the new blocks.
## Current Oracle Backend Families
### Live today
- `/api/oracle/v1/me`
- `/api/oracle/v1/canvas-pages/{page_id}`
- `/api/oracle/v1/canvas-pages/{page_id}/prompts`
- `/api/oracle/v1/canvas-pages/{page_id}/forks`
- `/api/oracle/v1/canvas-pages/{page_id}/rollback`
- `/api/oracle/v1/canvas-pages/{page_id}/revisions`
- `/api/oracle/v1/component-templates`
- `/api/oracle/v1/component-templates/synthesize`
- `/api/oracle/v1/merge-requests`
- `/api/oracle/v1/merge-requests/{mr_id}/review`
- `/ws/oracle/canvas/{page_id}`
### Template taxonomy routes
- `/api/oracle/template-chapters`
- `/api/oracle/template-subchapters`
- `/api/oracle/component-templates`
- `/api/oracle/component-templates/{id}`
- `/api/oracle/component-templates/{id}/seed`
- `/api/oracle/component-templates/synthetic-jobs`
## Prompt Analysis Path
Oracle should not rely on one monolithic LLM call.
The correct production split is:
1. codebook retrieval
2. safe dataset selection
3. optional LLM planning
4. live DB fetch
5. JSON Canvas synthesis
6. revision commit
### Why this split is correct
- It reduces hallucination in UI structure.
- It keeps DB access whitelisted and auditable.
- It allows Oracle to keep working even when the LLM runtime is degraded.
- It keeps the Oracle Canvas deterministic enough for operational use.
## Current Model Routing Truth
### Present reality
The current Oracle backend has these runtime modes:
- `codebook_retrieval`
- preferred when the prompt clearly matches the Oracle template corpus
- `nemoclaw_hosted`
- used when `NEMOCLAW_API_URL` and `NEMOCLAW_API_KEY` are configured and reachable
- `deterministic_fallback`
- used when the LLM planner is unavailable
### What Nemoclaw currently means in code
Current dispatch abstraction:
- `backend/services/nemoclaw_runtime.py`
This file is still a light dispatch envelope, not a fully featured provider router.
### Recommended production provider stack
Provider order:
1. codebook retrieval layer
2. Nemoclaw planner endpoint
3. local Ollama fallback
4. deterministic fallback
## Recommended Ollama Model Policy
### Default planning / Oracle analysis model
Use a local reasoning-capable model behind Ollama when Nemoclaw is not available or when the team wants deterministic private execution.
Recommended candidate:
- `qwen3.6:35b-a3b`
Reason:
- strong agentic coding and structured reasoning profile
- local execution path through Ollama
- realistic fit for GPU-box-hosted inference
### Deployment command
Example:
```bash
ollama run qwen3.6:35b-a3b
```
### Routing rule
- Oracle prompt planning:
- small to medium prompts: local Ollama `qwen3.6:35b-a3b`
- larger multi-step analytical plans: Nemoclaw planner if available
- Coding-agent batch workloads:
- Ollama first for local/private jobs
- Nemoclaw for heavier orchestration when the runtime is healthy
## Runtime LLM API
The backend now exposes a first-class runtime LLM family:
- `GET /api/runtime/llm/providers`
- `POST /api/runtime/llm/chat`
- `POST /api/runtime/llm/batch`
- `GET /api/runtime/llm/jobs/{job_id}`
- `GET /api/runtime/llm/jobs/{job_id}/results`
This router is mounted in:
- `backend/api/routes_runtime_llm.py`
The current persistence path uses the existing canonical table:
- `workflow_agent_runs`
That means batch jobs are now persisted against the live Velocity schema without requiring a new table family before the first production rollout.
## Implemented Batch Processing API
This is no longer only a proposal. The following contract family exists now and can be used by Oracle or future coding-agent surfaces.
### Single request inference
- `POST /api/runtime/llm/chat`
Payload:
```json
{
"provider": "ollama",
"model": "qwen3.6:35b-a3b",
"system_prompt": "You are Oracle Planner.",
"messages": [
{ "role": "user", "content": "Build a CRM pipeline view for high-intent NRI buyers." }
],
"temperature": 0.2,
"response_format": "json"
}
```
### Batch submission
- `POST /api/runtime/llm/batch`
Payload:
```json
{
"provider": "ollama",
"model": "qwen3.6:35b-a3b",
"job_type": "oracle_canvas_planning",
"items": [
{
"request_id": "req_001",
"messages": [
{ "role": "user", "content": "Show overdue high-QD follow-ups." }
],
"response_format": "json"
},
{
"request_id": "req_002",
"messages": [
{ "role": "user", "content": "Build a Kolkata luxury inventory comparison block." }
],
"response_format": "json"
}
]
}
```
### Batch status
- `GET /api/runtime/llm/jobs/{job_id}`
Response:
```json
{
"job_id": "job_123",
"status": "running",
"provider": "ollama",
"model": "qwen3.6:35b-a3b",
"submitted_count": 2,
"completed_count": 1,
"failed_count": 0
}
```
### Batch results
- `GET /api/runtime/llm/jobs/{job_id}/results`
### Providers inventory
- `GET /api/runtime/llm/providers`
Example response:
```json
{
"providers": [
{
"id": "nemoclaw",
"status": "online",
"models": ["nemotron", "remote_default"]
},
{
"id": "ollama",
"status": "online",
"models": ["qwen3.6:35b-a3b"]
}
]
}
```
## Batch Processing Design Rules
1. Batch jobs must be persisted.
2. Batch items must be individually addressable by `request_id`.
3. Every batch job must record:
- provider
- model
- submitted payload hash
- start/end timestamps
- failure reason
4. Oracle must not block the main request thread for large batches.
5. Any DB writeback generated from a batch must go through approval tables, not direct execution.
## Oracle-Specific Runtime Policy
For Oracle Canvas, the LLM is not the source of truth for data.
The source of truth order is:
1. canonical DB tables
2. approved dataset projections
3. codebook template corpus
4. model planner
The model is only allowed to:
- classify intent
- choose likely component families
- propose layout direction
- summarize findings
The model is not allowed to:
- invent database facts
- bypass dataset allowlists
- emit arbitrary executable code into production rendering paths
## Current Production Readiness Assessment
### Ready now
- Oracle Canvas frontend-to-backend v1 route family
- codebook-backed template retrieval path
- safe DB execution gateway
- merge/fork/revision path
- deterministic fallback path
- runtime LLM provider inventory
- runtime single-chat execution
- runtime persisted batch execution through `workflow_agent_runs`
- Oracle planner fallback through the shared runtime LLM service
### Still needs explicit implementation if the team approves
- per-model selection UI in Catalyst or Oracle controls
- dedicated `runtime_llm_jobs` / `runtime_llm_job_items` tables if the team wants stronger audit/query ergonomics than `workflow_agent_runs`
- explicit Nemoclaw vs Ollama operator switch in a production admin surface
- richer provider health telemetry beyond simple reachability
## Recommended Next Build Steps
1. Add a dedicated runtime router:
- `backend/api/routes_runtime_llm.py`
2. Add DB tables:
- `runtime_llm_jobs`
- `runtime_llm_job_items`
- `runtime_llm_job_results`
3. Implement provider adapters:
- Nemoclaw adapter
- Ollama adapter
4. Expose provider status to Catalyst/Oracle settings surfaces.
5. Keep Oracle Canvas on the current codebook-first path even after LLM batching exists.
## Bottom Line
Oracle Canvas should be treated as a codebook-guided analytical surface with optional LLM planning, not as a raw chat-to-SQL toy.
The production-safe architecture is:
- Linux origin runs the application and DB access
- GPU box runs ComfyUI and model inference
- Oracle retrieves from the merged codebook first
- DB access stays whitelisted
- Nemoclaw and Ollama sit behind a documented provider interface
- batch processing is a separate runtime service contract, not an implicit side effect of the canvas endpoint

View File

@@ -0,0 +1,225 @@
# Oracle Template Seed DB — Expanded Examples v2.0
**Project:** Velocity — Multi-Surface Platform and Oracle Expansion
**Date:** 2026-04-19
**Owner:** Sayan (generated as part of Sprint 1 Oracle Template Taxonomy deliverable)
**Depends on:** `schema_extension_v2.sql`, `oracle_template_seed_db.json` (v1.0 canonical seeds)
**Total Examples:** 1,200 (50 per subchapter × 24 subchapters × 6 chapters)
---
## What This Is
This package expands the original 8-example Oracle Template Seed DB (`oracle_template_seed_db.json`) into a full 1,200-example corpus covering every subchapter in the Oracle template taxonomy. It is the implementation artifact for Sprint 1 deliverable **§2.4 — Oracle Template Taxonomy and Seed JSON Structure**.
Every example conforms to the established Velocity Oracle component contract shape. They are ready to be ingested into `oracle_template_seed_examples` via the `POST /api/oracle/component-templates/seed` route and consumed by Kimi Synthetic Data expansion jobs (`oracle_synthetic_generation_jobs`).
---
## File Layout
```
oracle_template_expansion/
├── README.md ← This file
├── oracle_template_seed_db_expanded.json ← Master combined file (all 1,200 examples)
├── sub-001-01_pricing_trends.json ← 50 examples
├── sub-001-02_demand_signals.json ← 50 examples
├── sub-001-03_competitive_landscape.json ← 50 examples
├── sub-001-04_location_index.json ← 50 examples
├── sub-002-01_lead_profile.json ← 50 examples
├── sub-002-02_qd_score.json ← 50 examples
├── sub-002-03_pipeline_health.json ← 50 examples
├── sub-002-04_engagement_history.json ← 50 examples
├── sub-003-01_call_summary.json ← 50 examples
├── sub-003-02_promise_tracker.json ← 50 examples
├── sub-003-03_whatsapp_thread.json ← 50 examples
├── sub-003-04_reminder_surface.json ← 50 examples
├── sub-004-01_property_card.json ← 50 examples
├── sub-004-02_availability_matrix.json ← 50 examples
├── sub-004-03_absorption_rate.json ← 50 examples
├── sub-004-04_inventory_comparison.json ← 50 examples
├── sub-005-01_showroom_traffic.json ← 50 examples
├── sub-005-02_team_performance.json ← 50 examples
├── sub-005-03_campaign_metrics.json ← 50 examples
├── sub-005-04_system_health.json ← 50 examples
├── sub-006-01_calendar_view.json ← 50 examples
├── sub-006-02_action_queue.json ← 50 examples
├── sub-006-03_follow-up_plan.json ← 50 examples
└── sub-006-04_reminder_cards.json ← 50 examples
```
---
## Chapter and Subchapter Map
| Chapter | Name | Subchapters | Examples |
|---------|------|-------------|----------|
| ch-001 | Market Intelligence | Pricing Trends, Demand Signals, Competitive Landscape, Location Index | 200 |
| ch-002 | Lead Intelligence | Lead Profile, QD Score, Pipeline Health, Engagement History | 200 |
| ch-003 | Communication Intelligence | Call Summary, Promise Tracker, WhatsApp Thread, Reminder Surface | 200 |
| ch-004 | Inventory Analytics | Property Card, Availability Matrix, Absorption Rate, Inventory Comparison | 200 |
| ch-005 | Operational Metrics | Showroom Traffic, Team Performance, Campaign Metrics, System Health | 200 |
| ch-006 | Calendar and Follow-Up | Calendar View, Action Queue, Follow-Up Plan, Reminder Cards | 200 |
| **Total** | | **24 subchapters** | **1,200** |
---
## Component Type Coverage
| componentType | Subchapters Used In | Approx Count |
|---------------|---------------------|--------------|
| `line_chart` | sub-001-01, sub-001-02, sub-004-03, sub-005-01, sub-005-02, sub-005-03 | ~120 |
| `bar_chart` | sub-001-02, sub-001-03, sub-004-03, sub-005-01, sub-005-02, sub-005-03 | ~100 |
| `area_chart` | sub-001-01, sub-004-03, sub-005-01 | ~45 |
| `heatmap` | sub-001-04, sub-005-01 | ~40 |
| `metric_card_group` | sub-002-02, sub-005-02, sub-005-03 | ~60 |
| `data_table` | sub-003-02 | ~50 |
| `property_card` | sub-004-01 | ~50 |
| `lead_profile_card` | sub-002-01 | ~50 |
| `communication_summary` | sub-003-01 | ~50 |
| `whatsapp_thread_viewer` | sub-003-03 | ~50 |
| `reminder_surface` | sub-003-04 | ~50 |
| `compact_alert_card` | sub-006-04 | ~50 |
| `action_queue` | sub-006-02 | ~50 |
| `calendar_view` | sub-006-01 | ~50 |
| `follow_up_plan` | sub-006-03 | ~50 |
| `availability_matrix` | sub-004-02 | ~50 |
| `inventory_comparison` | sub-004-04 | ~50 |
| `system_health_panel` | sub-005-04 | ~50 |
| `radar_chart`, `scatter_chart`, `funnel_chart`, others | various | ~135 |
---
## Example JSON Structure
Every example in every subchapter file follows this envelope:
```json
{
"example_id": "ex-0009",
"chapter_id": "ch-001",
"subchapter_id": "sub-001-01",
"title": "Component title string",
"quality_notes": "Human-readable note about this variant",
"is_canonical": true,
"template_name": "Subchapter Name — Template N",
"component_type": "line_chart",
"accepted_shapes": ["time_series"],
"example_json": {
"componentType": "line_chart",
"title": "...",
"subtitle": "...",
"dataSource": { ... },
"visualization": { ... },
"style": { ... },
"surfaceTargets": [ ... ]
}
}
```
The first example in each subchapter (`is_canonical: true`) is the recommended reference template for that subchapter.
---
## Design Language Compliance
All examples follow the established Velocity Oracle design language:
**Color palette** — All `accentColor` values come from the 10-color Velocity token set:
- `#2563EB` (primary blue), `#10B981` (emerald), `#F59E0B` (amber), `#EF4444` (red)
- `#8B5CF6` (violet), `#0EA5E9` (sky), `#EC4899` (pink), `#14B8A6` (teal)
- `#F97316` (orange), `#6366F1` (indigo)
**Semantic colors** — Status colors are fixed:
- Healthy / positive: `#10B981`
- Warning: `#F59E0B`
- Critical / negative: `#EF4444`
- Neutral / muted: `#94A3B8`
**Data source types** — Examples use only the contracted Oracle data source types:
- `inventory_aggregate`, `inventory_property`, `inventory_multi_property`
- `crm_lead`, `crm_aggregate`, `crm_engagement`, `crm_pipeline`, `crm_team_performance`
- `sentinel_qd`, `sentinel_live`, `sentinel_historical`
- `edge_communication_event`, `edge_memory_facts`
- `user_calendar_events`, `insight_recommendations`
- `nemoclaw_plan`, `catalyst_campaign`, `admin_health`, `competitive_intelligence`, `location_intelligence`
**Template variables** — Dynamic entity references use double-brace mustache syntax: `{{lead_id}}`, `{{property_id}}`, `{{agent_id}}`, `{{event_id}}`, `{{tenant_id}}`, `{{user_id}}`.
**Surface targets** — Every example declares `surfaceTargets` from the set: `webos`, `ipad`, `android_tablet`, `iphone_edge`, `android_phone_edge`.
---
## Permutation Logic
Each subchapter's 50 examples are generated by cycling through permutation combinations of:
- **District / developer / lead / agent names** — drawn from real Dubai market data (districts, developer names, nationality mix aligned to UAE CRM reality)
- **Time windows** — `7D`, `14D`, `30D`, `60D`, `90D`, `6M`, `12M`, `24M`, `YTD`, `QTD`
- **Chart types** — 46 types per subchapter appropriate to the data shape
- **Grouping dimensions** — e.g. by agent, district, property type, nationality
- **Layout variants** — e.g. `hero_with_stats`, `compact_card`, `list_row` for property cards
- **Action sets** — e.g. `accept / dismiss / snooze_1h` vs `call_now / send_whatsapp / dismiss`
- **Optional fields** — annotations, benchmarks, comparisons, sparklines toggled on/off across the 50
This means every subchapter has diverse examples covering different use cases while staying within the correct data contract for that component family.
---
## How to Ingest Into the Database
### Option 1 — Per-subchapter seed via Admin Surface
```bash
POST /api/oracle/component-templates/seed
Content-Type: application/json
{
"subchapter_id": "<uuid from oracle_template_subchapters>",
"examples": [ ... ] # paste the "examples" array from the per-subchapter file
}
```
### Option 2 — Bulk ingest via Kimi Synthetic Job
The master file (`oracle_template_seed_db_expanded.json`) is the correct input for `oracle_synthetic_generation_jobs`. Insert a job row referencing the template and let the background worker distribute examples into `oracle_template_seed_examples`.
### Option 3 — Direct SQL seed (dev/staging only)
```sql
INSERT INTO oracle_template_seed_examples
(template_id, chapter_id, subchapter_id, title, example_json, quality_notes, is_canonical)
VALUES
(...);
```
Map string chapter/subchapter IDs from the JSON against the UUID rows you insert via the migration in `schema_extension_v2.sql`.
---
## Known Caveats and Next Steps
- **`_meta.total_seed_examples` in v1 seed DB** — The original `oracle_template_seed_db.json` reports `36` in `_meta.total_seed_examples` but only contains 8 examples. This mismatch was noted in `delivery_log.md`. This expansion does not patch the v1 file; correct it separately before merging both corpora.
- **Kimi expansion** — These 1,200 examples are the **seed corpus**, not the synthetic expansion. Run `oracle_synthetic_generation_jobs` against published templates to generate the larger training/demo sets described in `KIMI_SYNTHETIC_DATA_DOWNSTREAM_PLAN.md`.
- **UUID mapping** — The `chapter_id` and `subchapter_id` fields in these files use the string keys from the v1 seed DB (`ch-001`, `sub-001-01`). Your migration script must map these to the PostgreSQL UUIDs inserted by `schema_extension_v2.sql`.
- **Template IDs** — `example_json.template_name` is a human label. Actual `template_id` UUIDs are assigned at ingestion time against `oracle_component_templates`.
---
## Generation Script
The generator script is included at `generate_examples.py` in the repo root (outside this zip). It is reproducible — re-running it with the same seed logic will produce the same 1,200 examples.
---
*Generated by Project Velocity platform tooling · 2026-04-19*

View File

@@ -0,0 +1,82 @@
# Oracle Canvas JSON Expansion Pack
This pack expands the current Oracle template seed library into a reviewable example set with **50 examples per subchapter** across all **24 subchapters**.
## What is inside
- `db/oracle_template_seed_db_expanded_v1.pretty.json`
Full expanded master DB with chapter taxonomy and all **1200** examples.
- `db/oracle_template_seed_db_expanded_v1.min.json`
Minified version of the same master DB.
- `examples/`
Chapter-by-chapter split files. Each subchapter file contains exactly **50** examples.
- `manifests/template_family_catalog.json`
Component families, accepted shapes, policy tags, and backend hints per subchapter.
- `manifests/subchapter_index.json`
Index of all generated files.
- `manifests/validation_report.json`
Validation summary for counts and ID uniqueness.
- `csv/subchapter_example_counts.csv`
Spreadsheet-friendly count manifest.
## Source alignment
This pack was generated against the current repo direction and constraints:
- FastAPI backend remains canonical.
- Oracle remains the analytical center.
- Mobile edge surfaces remain narrow, bounded control surfaces.
- Communication intelligence examples stay inside supported channels and provenance-aware capture modes.
- Admin examples only model bounded and auditable actions.
- The expanded examples follow the live-data-first / no-mock direction from the delivery log.
## Important correction carried forward
The source seed DB metadata currently reports `total_seed_examples: 36`, but the source file actually contains **8** canonical seed examples.
This expansion pack corrects the count in its own metadata and preserves the existing canonical examples inside the 50-example-per-subchapter allocation wherever they already existed.
## Design language used
Common policy tags applied through the pack:
- `backend_owned`
- `live_data_first`
- `no_mock_fallback`
- `surface_safe`
Additional policy tags appear per subchapter where relevant, including:
- `supported_channel_only`
- `provider_provenance_required`
- `bounded_admin_actions`
- `confirmation_required_for_writeback`
- `business_whatsapp_scope`
- `nemoclaw_suggested`
## Notes on IDs
The source taxonomy uses symbolic IDs such as `ch-001` and `sub-001-01`.
This pack preserves those symbolic IDs for review and lineage consistency.
Generated example IDs use deterministic `exg-*` identifiers. Existing canonical example IDs from the source file are preserved.
## Suggested use
1. Review examples subchapter-by-subchapter from `examples/`.
2. Use `template_family_catalog.json` to decide which component families should become formal reusable templates.
3. Use the master DB JSON once you are ready to merge the chosen examples into the Oracle seed library.
4. Keep the metadata notes about symbolic taxonomy IDs in mind when preparing any DB import step against UUID-backed SQL tables.
## Counts
- Chapters: 6
- Subchapters: 24
- Total examples: 1200
- Canonical carried forward: 8
- Generated additions: 1192

View File

@@ -0,0 +1,25 @@
chapter_id,chapter_name,subchapter_id,subchapter_name,example_count,file
ch-001,Market Intelligence,sub-001-01,Pricing Trends,50,examples/ch-001_market-intelligence/sub-001-01_pricing-trends.json
ch-001,Market Intelligence,sub-001-02,Demand Signals,50,examples/ch-001_market-intelligence/sub-001-02_demand-signals.json
ch-001,Market Intelligence,sub-001-03,Competitive Landscape,50,examples/ch-001_market-intelligence/sub-001-03_competitive-landscape.json
ch-001,Market Intelligence,sub-001-04,Location Index,50,examples/ch-001_market-intelligence/sub-001-04_location-index.json
ch-002,Lead Intelligence,sub-002-01,Lead Profile,50,examples/ch-002_lead-intelligence/sub-002-01_lead-profile.json
ch-002,Lead Intelligence,sub-002-02,QD Score,50,examples/ch-002_lead-intelligence/sub-002-02_qd-score.json
ch-002,Lead Intelligence,sub-002-03,Pipeline Health,50,examples/ch-002_lead-intelligence/sub-002-03_pipeline-health.json
ch-002,Lead Intelligence,sub-002-04,Engagement History,50,examples/ch-002_lead-intelligence/sub-002-04_engagement-history.json
ch-003,Communication Intelligence,sub-003-01,Call Summary,50,examples/ch-003_communication-intelligence/sub-003-01_call-summary.json
ch-003,Communication Intelligence,sub-003-02,Promise Tracker,50,examples/ch-003_communication-intelligence/sub-003-02_promise-tracker.json
ch-003,Communication Intelligence,sub-003-03,WhatsApp Thread,50,examples/ch-003_communication-intelligence/sub-003-03_whatsapp-thread.json
ch-003,Communication Intelligence,sub-003-04,Reminder Surface,50,examples/ch-003_communication-intelligence/sub-003-04_reminder-surface.json
ch-004,Inventory Analytics,sub-004-01,Property Card,50,examples/ch-004_inventory-analytics/sub-004-01_property-card.json
ch-004,Inventory Analytics,sub-004-02,Availability Matrix,50,examples/ch-004_inventory-analytics/sub-004-02_availability-matrix.json
ch-004,Inventory Analytics,sub-004-03,Absorption Rate,50,examples/ch-004_inventory-analytics/sub-004-03_absorption-rate.json
ch-004,Inventory Analytics,sub-004-04,Inventory Comparison,50,examples/ch-004_inventory-analytics/sub-004-04_inventory-comparison.json
ch-005,Operational Metrics,sub-005-01,Showroom Traffic,50,examples/ch-005_operational-metrics/sub-005-01_showroom-traffic.json
ch-005,Operational Metrics,sub-005-02,Team Performance,50,examples/ch-005_operational-metrics/sub-005-02_team-performance.json
ch-005,Operational Metrics,sub-005-03,Campaign Metrics,50,examples/ch-005_operational-metrics/sub-005-03_campaign-metrics.json
ch-005,Operational Metrics,sub-005-04,System Health,50,examples/ch-005_operational-metrics/sub-005-04_system-health.json
ch-006,Calendar and Follow-Up,sub-006-01,Calendar View,50,examples/ch-006_calendar-and-follow-up/sub-006-01_calendar-view.json
ch-006,Calendar and Follow-Up,sub-006-02,Action Queue,50,examples/ch-006_calendar-and-follow-up/sub-006-02_action-queue.json
ch-006,Calendar and Follow-Up,sub-006-03,Follow-Up Plan,50,examples/ch-006_calendar-and-follow-up/sub-006-03_follow-up-plan.json
ch-006,Calendar and Follow-Up,sub-006-04,Reminder Cards,50,examples/ch-006_calendar-and-follow-up/sub-006-04_reminder-cards.json
1 chapter_id chapter_name subchapter_id subchapter_name example_count file
2 ch-001 Market Intelligence sub-001-01 Pricing Trends 50 examples/ch-001_market-intelligence/sub-001-01_pricing-trends.json
3 ch-001 Market Intelligence sub-001-02 Demand Signals 50 examples/ch-001_market-intelligence/sub-001-02_demand-signals.json
4 ch-001 Market Intelligence sub-001-03 Competitive Landscape 50 examples/ch-001_market-intelligence/sub-001-03_competitive-landscape.json
5 ch-001 Market Intelligence sub-001-04 Location Index 50 examples/ch-001_market-intelligence/sub-001-04_location-index.json
6 ch-002 Lead Intelligence sub-002-01 Lead Profile 50 examples/ch-002_lead-intelligence/sub-002-01_lead-profile.json
7 ch-002 Lead Intelligence sub-002-02 QD Score 50 examples/ch-002_lead-intelligence/sub-002-02_qd-score.json
8 ch-002 Lead Intelligence sub-002-03 Pipeline Health 50 examples/ch-002_lead-intelligence/sub-002-03_pipeline-health.json
9 ch-002 Lead Intelligence sub-002-04 Engagement History 50 examples/ch-002_lead-intelligence/sub-002-04_engagement-history.json
10 ch-003 Communication Intelligence sub-003-01 Call Summary 50 examples/ch-003_communication-intelligence/sub-003-01_call-summary.json
11 ch-003 Communication Intelligence sub-003-02 Promise Tracker 50 examples/ch-003_communication-intelligence/sub-003-02_promise-tracker.json
12 ch-003 Communication Intelligence sub-003-03 WhatsApp Thread 50 examples/ch-003_communication-intelligence/sub-003-03_whatsapp-thread.json
13 ch-003 Communication Intelligence sub-003-04 Reminder Surface 50 examples/ch-003_communication-intelligence/sub-003-04_reminder-surface.json
14 ch-004 Inventory Analytics sub-004-01 Property Card 50 examples/ch-004_inventory-analytics/sub-004-01_property-card.json
15 ch-004 Inventory Analytics sub-004-02 Availability Matrix 50 examples/ch-004_inventory-analytics/sub-004-02_availability-matrix.json
16 ch-004 Inventory Analytics sub-004-03 Absorption Rate 50 examples/ch-004_inventory-analytics/sub-004-03_absorption-rate.json
17 ch-004 Inventory Analytics sub-004-04 Inventory Comparison 50 examples/ch-004_inventory-analytics/sub-004-04_inventory-comparison.json
18 ch-005 Operational Metrics sub-005-01 Showroom Traffic 50 examples/ch-005_operational-metrics/sub-005-01_showroom-traffic.json
19 ch-005 Operational Metrics sub-005-02 Team Performance 50 examples/ch-005_operational-metrics/sub-005-02_team-performance.json
20 ch-005 Operational Metrics sub-005-03 Campaign Metrics 50 examples/ch-005_operational-metrics/sub-005-03_campaign-metrics.json
21 ch-005 Operational Metrics sub-005-04 System Health 50 examples/ch-005_operational-metrics/sub-005-04_system-health.json
22 ch-006 Calendar and Follow-Up sub-006-01 Calendar View 50 examples/ch-006_calendar-and-follow-up/sub-006-01_calendar-view.json
23 ch-006 Calendar and Follow-Up sub-006-02 Action Queue 50 examples/ch-006_calendar-and-follow-up/sub-006-02_action-queue.json
24 ch-006 Calendar and Follow-Up sub-006-03 Follow-Up Plan 50 examples/ch-006_calendar-and-follow-up/sub-006-03_follow-up-plan.json
25 ch-006 Calendar and Follow-Up sub-006-04 Reminder Cards 50 examples/ch-006_calendar-and-follow-up/sub-006-04_reminder-cards.json

View File

@@ -0,0 +1,194 @@
[
{
"chapter_id": "ch-001",
"chapter_name": "Market Intelligence",
"subchapter_id": "sub-001-01",
"subchapter_name": "Pricing Trends",
"example_count": 50,
"file": "examples/ch-001_market-intelligence/sub-001-01_pricing-trends.json"
},
{
"chapter_id": "ch-001",
"chapter_name": "Market Intelligence",
"subchapter_id": "sub-001-02",
"subchapter_name": "Demand Signals",
"example_count": 50,
"file": "examples/ch-001_market-intelligence/sub-001-02_demand-signals.json"
},
{
"chapter_id": "ch-001",
"chapter_name": "Market Intelligence",
"subchapter_id": "sub-001-03",
"subchapter_name": "Competitive Landscape",
"example_count": 50,
"file": "examples/ch-001_market-intelligence/sub-001-03_competitive-landscape.json"
},
{
"chapter_id": "ch-001",
"chapter_name": "Market Intelligence",
"subchapter_id": "sub-001-04",
"subchapter_name": "Location Index",
"example_count": 50,
"file": "examples/ch-001_market-intelligence/sub-001-04_location-index.json"
},
{
"chapter_id": "ch-002",
"chapter_name": "Lead Intelligence",
"subchapter_id": "sub-002-01",
"subchapter_name": "Lead Profile",
"example_count": 50,
"file": "examples/ch-002_lead-intelligence/sub-002-01_lead-profile.json"
},
{
"chapter_id": "ch-002",
"chapter_name": "Lead Intelligence",
"subchapter_id": "sub-002-02",
"subchapter_name": "QD Score",
"example_count": 50,
"file": "examples/ch-002_lead-intelligence/sub-002-02_qd-score.json"
},
{
"chapter_id": "ch-002",
"chapter_name": "Lead Intelligence",
"subchapter_id": "sub-002-03",
"subchapter_name": "Pipeline Health",
"example_count": 50,
"file": "examples/ch-002_lead-intelligence/sub-002-03_pipeline-health.json"
},
{
"chapter_id": "ch-002",
"chapter_name": "Lead Intelligence",
"subchapter_id": "sub-002-04",
"subchapter_name": "Engagement History",
"example_count": 50,
"file": "examples/ch-002_lead-intelligence/sub-002-04_engagement-history.json"
},
{
"chapter_id": "ch-003",
"chapter_name": "Communication Intelligence",
"subchapter_id": "sub-003-01",
"subchapter_name": "Call Summary",
"example_count": 50,
"file": "examples/ch-003_communication-intelligence/sub-003-01_call-summary.json"
},
{
"chapter_id": "ch-003",
"chapter_name": "Communication Intelligence",
"subchapter_id": "sub-003-02",
"subchapter_name": "Promise Tracker",
"example_count": 50,
"file": "examples/ch-003_communication-intelligence/sub-003-02_promise-tracker.json"
},
{
"chapter_id": "ch-003",
"chapter_name": "Communication Intelligence",
"subchapter_id": "sub-003-03",
"subchapter_name": "WhatsApp Thread",
"example_count": 50,
"file": "examples/ch-003_communication-intelligence/sub-003-03_whatsapp-thread.json"
},
{
"chapter_id": "ch-003",
"chapter_name": "Communication Intelligence",
"subchapter_id": "sub-003-04",
"subchapter_name": "Reminder Surface",
"example_count": 50,
"file": "examples/ch-003_communication-intelligence/sub-003-04_reminder-surface.json"
},
{
"chapter_id": "ch-004",
"chapter_name": "Inventory Analytics",
"subchapter_id": "sub-004-01",
"subchapter_name": "Property Card",
"example_count": 50,
"file": "examples/ch-004_inventory-analytics/sub-004-01_property-card.json"
},
{
"chapter_id": "ch-004",
"chapter_name": "Inventory Analytics",
"subchapter_id": "sub-004-02",
"subchapter_name": "Availability Matrix",
"example_count": 50,
"file": "examples/ch-004_inventory-analytics/sub-004-02_availability-matrix.json"
},
{
"chapter_id": "ch-004",
"chapter_name": "Inventory Analytics",
"subchapter_id": "sub-004-03",
"subchapter_name": "Absorption Rate",
"example_count": 50,
"file": "examples/ch-004_inventory-analytics/sub-004-03_absorption-rate.json"
},
{
"chapter_id": "ch-004",
"chapter_name": "Inventory Analytics",
"subchapter_id": "sub-004-04",
"subchapter_name": "Inventory Comparison",
"example_count": 50,
"file": "examples/ch-004_inventory-analytics/sub-004-04_inventory-comparison.json"
},
{
"chapter_id": "ch-005",
"chapter_name": "Operational Metrics",
"subchapter_id": "sub-005-01",
"subchapter_name": "Showroom Traffic",
"example_count": 50,
"file": "examples/ch-005_operational-metrics/sub-005-01_showroom-traffic.json"
},
{
"chapter_id": "ch-005",
"chapter_name": "Operational Metrics",
"subchapter_id": "sub-005-02",
"subchapter_name": "Team Performance",
"example_count": 50,
"file": "examples/ch-005_operational-metrics/sub-005-02_team-performance.json"
},
{
"chapter_id": "ch-005",
"chapter_name": "Operational Metrics",
"subchapter_id": "sub-005-03",
"subchapter_name": "Campaign Metrics",
"example_count": 50,
"file": "examples/ch-005_operational-metrics/sub-005-03_campaign-metrics.json"
},
{
"chapter_id": "ch-005",
"chapter_name": "Operational Metrics",
"subchapter_id": "sub-005-04",
"subchapter_name": "System Health",
"example_count": 50,
"file": "examples/ch-005_operational-metrics/sub-005-04_system-health.json"
},
{
"chapter_id": "ch-006",
"chapter_name": "Calendar and Follow-Up",
"subchapter_id": "sub-006-01",
"subchapter_name": "Calendar View",
"example_count": 50,
"file": "examples/ch-006_calendar-and-follow-up/sub-006-01_calendar-view.json"
},
{
"chapter_id": "ch-006",
"chapter_name": "Calendar and Follow-Up",
"subchapter_id": "sub-006-02",
"subchapter_name": "Action Queue",
"example_count": 50,
"file": "examples/ch-006_calendar-and-follow-up/sub-006-02_action-queue.json"
},
{
"chapter_id": "ch-006",
"chapter_name": "Calendar and Follow-Up",
"subchapter_id": "sub-006-03",
"subchapter_name": "Follow-Up Plan",
"example_count": 50,
"file": "examples/ch-006_calendar-and-follow-up/sub-006-03_follow-up-plan.json"
},
{
"chapter_id": "ch-006",
"chapter_name": "Calendar and Follow-Up",
"subchapter_id": "sub-006-04",
"subchapter_name": "Reminder Cards",
"example_count": 50,
"file": "examples/ch-006_calendar-and-follow-up/sub-006-04_reminder-cards.json"
}
]

View File

@@ -0,0 +1,931 @@
[
{
"chapter_id": "ch-001",
"chapter_name": "Market Intelligence",
"subchapter_id": "sub-001-01",
"subchapter_name": "Pricing Trends",
"component_types": [
"area_chart",
"benchmark_band_chart",
"dual_axis_chart",
"line_chart",
"sparkline_metric"
],
"accepted_shapes": [
"comparative_time_series",
"district_benchmark",
"dual_metric_time_series",
"segment_snapshot",
"time_series"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe"
],
"route_family": "oracle",
"primary_tables": [
"oracle_component_templates",
"inventory_properties"
]
},
{
"chapter_id": "ch-001",
"chapter_name": "Market Intelligence",
"subchapter_id": "sub-001-02",
"subchapter_name": "Demand Signals",
"component_types": [
"bar_chart",
"funnel_chart",
"heatmap",
"line_chart",
"metric_card_group"
],
"accepted_shapes": [
"categorical_count",
"conversion_funnel",
"demand_snapshot",
"intent_time_series",
"zone_time_matrix"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe"
],
"route_family": "oracle",
"primary_tables": [
"oracle_component_templates",
"inventory_properties"
]
},
{
"chapter_id": "ch-001",
"chapter_name": "Market Intelligence",
"subchapter_id": "sub-001-03",
"subchapter_name": "Competitive Landscape",
"component_types": [
"bar_chart",
"comparison_table",
"grouped_bar_chart",
"matrix_grid",
"scorecard_panel"
],
"accepted_shapes": [
"competitive_matrix",
"competitive_scorecard",
"developer_benchmark",
"developer_pipeline",
"unit_mix_distribution"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe"
],
"route_family": "oracle",
"primary_tables": [
"oracle_component_templates",
"inventory_properties"
]
},
{
"chapter_id": "ch-001",
"chapter_name": "Market Intelligence",
"subchapter_id": "sub-001-04",
"subchapter_name": "Location Index",
"component_types": [
"data_table",
"map_score_card",
"radar_chart",
"scorecard_panel",
"timeline_chart"
],
"accepted_shapes": [
"district_ranking",
"infrastructure_readiness",
"location_index",
"location_map_summary",
"proximity_profile"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe"
],
"route_family": "oracle",
"primary_tables": [
"oracle_component_templates",
"inventory_properties"
]
},
{
"chapter_id": "ch-002",
"chapter_name": "Lead Intelligence",
"subchapter_id": "sub-002-01",
"subchapter_name": "Lead Profile",
"component_types": [
"affinity_card",
"cluster_card",
"lead_profile_card",
"metric_card_group",
"summary_strip"
],
"accepted_shapes": [
"district_affinity",
"lead_preferences",
"lead_profile",
"lead_summary",
"persona_cluster"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet",
"iphone_edge",
"android_phone_edge"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe"
],
"route_family": "crm",
"primary_tables": [
"leads",
"sentinel_scores"
]
},
{
"chapter_id": "ch-002",
"chapter_name": "Lead Intelligence",
"subchapter_id": "sub-002-02",
"subchapter_name": "QD Score",
"component_types": [
"bar_chart",
"gauge_stack",
"line_chart",
"matrix_grid",
"metric_card_group"
],
"accepted_shapes": [
"qd_matrix",
"qd_peer_benchmark",
"qd_score_breakdown",
"qd_snapshot",
"qd_trend"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet",
"iphone_edge",
"android_phone_edge"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe"
],
"route_family": "crm",
"primary_tables": [
"leads",
"sentinel_scores"
]
},
{
"chapter_id": "ch-002",
"chapter_name": "Lead Intelligence",
"subchapter_id": "sub-002-03",
"subchapter_name": "Pipeline Health",
"component_types": [
"data_table",
"funnel_chart",
"heatmap",
"metric_card_group",
"stacked_bar_chart"
],
"accepted_shapes": [
"pipeline_distribution",
"pipeline_forecast",
"pipeline_probability_matrix",
"pipeline_stalls",
"pipeline_velocity"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe"
],
"route_family": "crm",
"primary_tables": [
"leads",
"sentinel_scores"
]
},
{
"chapter_id": "ch-002",
"chapter_name": "Lead Intelligence",
"subchapter_id": "sub-002-04",
"subchapter_name": "Engagement History",
"component_types": [
"data_table",
"heatmap",
"interaction_timeline",
"line_chart",
"metric_card_group"
],
"accepted_shapes": [
"channel_preference_trend",
"content_interaction_log",
"engagement_heatmap",
"engagement_snapshot",
"engagement_timeline"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet",
"iphone_edge",
"android_phone_edge"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe"
],
"route_family": "crm",
"primary_tables": [
"leads",
"sentinel_scores"
]
},
{
"chapter_id": "ch-003",
"chapter_name": "Communication Intelligence",
"subchapter_id": "sub-003-01",
"subchapter_name": "Call Summary",
"component_types": [
"communication_summary",
"data_table",
"metric_card_group",
"next_best_action_card",
"transcript_highlight_card"
],
"accepted_shapes": [
"call_follow_up_snapshot",
"call_outcome_snapshot",
"speaker_highlights",
"transcript_segments",
"transcript_summary"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet",
"iphone_edge",
"android_phone_edge"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe",
"provider_provenance_required",
"supported_channel_only"
],
"route_family": "mobile-edge",
"primary_tables": [
"edge_communication_events",
"edge_communication_memory_facts",
"edge_transcription_jobs",
"edge_transcript_segments"
]
},
{
"chapter_id": "ch-003",
"chapter_name": "Communication Intelligence",
"subchapter_id": "sub-003-02",
"subchapter_name": "Promise Tracker",
"component_types": [
"checklist_board",
"compact_alert_card",
"data_table",
"matrix_grid",
"summary_card"
],
"accepted_shapes": [
"communication_facts",
"decision_maker_summary",
"follow_up_checklist",
"overdue_commitments",
"promise_confidence_matrix"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet",
"iphone_edge",
"android_phone_edge"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe",
"reviewable_writebacks",
"communication_memory"
],
"route_family": "mobile-edge",
"primary_tables": [
"edge_communication_events",
"edge_communication_memory_facts",
"edge_transcription_jobs",
"edge_transcript_segments"
]
},
{
"chapter_id": "ch-003",
"chapter_name": "Communication Intelligence",
"subchapter_id": "sub-003-03",
"subchapter_name": "WhatsApp Thread",
"component_types": [
"data_table",
"line_chart",
"message_thread_summary",
"metric_card_group",
"summary_card"
],
"accepted_shapes": [
"message_action_queue",
"message_sentiment_timeline",
"operator_handover",
"thread_sla_snapshot",
"whatsapp_thread"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet",
"iphone_edge",
"android_phone_edge"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe",
"supported_channel_only",
"business_whatsapp_scope"
],
"route_family": "mobile-edge",
"primary_tables": [
"edge_communication_events",
"edge_communication_memory_facts",
"edge_transcription_jobs",
"edge_transcript_segments"
]
},
{
"chapter_id": "ch-003",
"chapter_name": "Communication Intelligence",
"subchapter_id": "sub-003-04",
"subchapter_name": "Reminder Surface",
"component_types": [
"action_strip",
"alert_queue",
"compact_alert_card",
"matrix_grid",
"next_best_action_card"
],
"accepted_shapes": [
"calendar_suggestion",
"insight_recommendation",
"next_best_action",
"recommendation_confidence",
"reminder_queue"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet",
"iphone_edge",
"android_phone_edge"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe",
"bounded_actions",
"nemoclaw_suggested"
],
"route_family": "mobile-edge",
"primary_tables": [
"edge_communication_events",
"edge_communication_memory_facts",
"edge_transcription_jobs",
"edge_transcript_segments"
]
},
{
"chapter_id": "ch-004",
"chapter_name": "Inventory Analytics",
"subchapter_id": "sub-004-01",
"subchapter_name": "Property Card",
"component_types": [
"bar_chart",
"cta_card",
"metric_card_group",
"property_card",
"summary_card"
],
"accepted_shapes": [
"inventory_property",
"property_cta",
"property_media_summary",
"property_pricing_snapshot",
"unit_mix_distribution"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet",
"iphone_edge",
"android_phone_edge"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe"
],
"route_family": "inventory",
"primary_tables": [
"inventory_properties",
"inventory_media_assets",
"inventory_import_batches"
]
},
{
"chapter_id": "ch-004",
"chapter_name": "Inventory Analytics",
"subchapter_id": "sub-004-02",
"subchapter_name": "Availability Matrix",
"component_types": [
"data_table",
"heatmap",
"matrix_grid",
"metric_card_group",
"stacked_bar_chart"
],
"accepted_shapes": [
"availability_heatmap",
"availability_matrix",
"bed_type_snapshot",
"price_band_grid",
"release_phase_distribution"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe"
],
"route_family": "inventory",
"primary_tables": [
"inventory_properties",
"inventory_media_assets",
"inventory_import_batches"
]
},
{
"chapter_id": "ch-004",
"chapter_name": "Inventory Analytics",
"subchapter_id": "sub-004-03",
"subchapter_name": "Absorption Rate",
"component_types": [
"bar_chart",
"dual_axis_chart",
"line_chart",
"matrix_grid",
"sparkline_metric"
],
"accepted_shapes": [
"developer_velocity_ranking",
"handover_absorption_matrix",
"rolling_velocity_snapshot",
"sales_velocity",
"velocity_supply_overlay"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe"
],
"route_family": "inventory",
"primary_tables": [
"inventory_properties",
"inventory_media_assets",
"inventory_import_batches"
]
},
{
"chapter_id": "ch-004",
"chapter_name": "Inventory Analytics",
"subchapter_id": "sub-004-04",
"subchapter_name": "Inventory Comparison",
"component_types": [
"comparison_table",
"metric_card_group",
"radar_chart",
"side_by_side_comparison",
"summary_strip"
],
"accepted_shapes": [
"amenity_comparison",
"inventory_comparison",
"operator_choice_summary",
"property_metric_comparison",
"sales_readiness_comparison"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe"
],
"route_family": "inventory",
"primary_tables": [
"inventory_properties",
"inventory_media_assets",
"inventory_import_batches"
]
},
{
"chapter_id": "ch-005",
"chapter_name": "Operational Metrics",
"subchapter_id": "sub-005-01",
"subchapter_name": "Showroom Traffic",
"component_types": [
"bar_chart",
"dual_axis_chart",
"heatmap",
"metric_card_group",
"summary_strip"
],
"accepted_shapes": [
"live_traffic_snapshot",
"peak_hour_distribution",
"visitor_flow_overlay",
"zone_summary",
"zone_time_matrix"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe"
],
"route_family": "admin-surface",
"primary_tables": [
"surface_sessions",
"admin_action_events",
"oracle_synthetic_generation_jobs",
"inventory_import_batches",
"edge_transcription_jobs"
]
},
{
"chapter_id": "ch-005",
"chapter_name": "Operational Metrics",
"subchapter_id": "sub-005-02",
"subchapter_name": "Team Performance",
"component_types": [
"compact_alert_card",
"dual_axis_chart",
"leaderboard_table",
"matrix_grid",
"metric_card_group"
],
"accepted_shapes": [
"activity_conversion_overlay",
"agent_leaderboard",
"follow_up_compliance_matrix",
"quality_drift_alert",
"team_performance_snapshot"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe"
],
"route_family": "admin-surface",
"primary_tables": [
"surface_sessions",
"admin_action_events",
"oracle_synthetic_generation_jobs",
"inventory_import_batches",
"edge_transcription_jobs"
]
},
{
"chapter_id": "ch-005",
"chapter_name": "Operational Metrics",
"subchapter_id": "sub-005-03",
"subchapter_name": "Campaign Metrics",
"component_types": [
"bar_chart",
"line_chart",
"metric_card_group",
"scatter_plot",
"summary_card"
],
"accepted_shapes": [
"campaign_attribution",
"campaign_efficiency",
"campaign_roas_trend",
"campaign_snapshot",
"channel_comparison"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe"
],
"route_family": "admin-surface",
"primary_tables": [
"campaign_metrics",
"lead_events"
]
},
{
"chapter_id": "ch-005",
"chapter_name": "Operational Metrics",
"subchapter_id": "sub-005-04",
"subchapter_name": "System Health",
"component_types": [
"action_panel",
"data_table",
"line_chart",
"metric_card_group",
"system_health_panel"
],
"accepted_shapes": [
"bounded_admin_actions",
"latency_time_series",
"queue_status",
"surface_session_snapshot",
"system_health_snapshot"
],
"surface_targets": [
"webos"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe",
"bounded_admin_actions",
"audit_ready"
],
"route_family": "admin-surface",
"primary_tables": [
"admin_action_events",
"oracle_synthetic_generation_jobs",
"inventory_import_batches",
"edge_transcription_jobs",
"surface_sessions"
]
},
{
"chapter_id": "ch-006",
"chapter_name": "Calendar and Follow-Up",
"subchapter_id": "sub-006-01",
"subchapter_name": "Calendar View",
"component_types": [
"calendar_agenda",
"calendar_heatmap",
"data_table",
"donut_chart",
"summary_strip"
],
"accepted_shapes": [
"calendar_density",
"calendar_mix",
"calendar_strip",
"calendar_suggestions",
"user_calendar_agenda"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet",
"iphone_edge",
"android_phone_edge"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe",
"user_calendar_scope",
"confirmation_required_for_writeback"
],
"route_family": "mobile-edge",
"primary_tables": [
"user_calendar_events",
"insight_recommendations",
"edge_communication_events"
]
},
{
"chapter_id": "ch-006",
"chapter_name": "Calendar and Follow-Up",
"subchapter_id": "sub-006-02",
"subchapter_name": "Action Queue",
"component_types": [
"action_strip",
"bar_chart",
"donut_chart",
"matrix_grid",
"prioritized_task_list"
],
"accepted_shapes": [
"action_status_mix",
"action_type_distribution",
"agent_action_queue",
"edge_action_strip",
"queue_urgency_matrix"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet",
"iphone_edge",
"android_phone_edge"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe",
"bounded_actions",
"operator_queue"
],
"route_family": "mobile-edge",
"primary_tables": [
"user_calendar_events",
"insight_recommendations",
"edge_communication_events"
]
},
{
"chapter_id": "ch-006",
"chapter_name": "Calendar and Follow-Up",
"subchapter_id": "sub-006-03",
"subchapter_name": "Follow-Up Plan",
"component_types": [
"compact_alert_card",
"data_table",
"structured_plan_card",
"summary_card",
"timeline_chart"
],
"accepted_shapes": [
"escalation_plan",
"follow_up_cadence",
"follow_up_edge_card",
"follow_up_plan",
"follow_up_timeline"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet",
"iphone_edge",
"android_phone_edge"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe",
"nemoclaw_suggested",
"confirmation_required_for_writeback"
],
"route_family": "mobile-edge",
"primary_tables": [
"user_calendar_events",
"insight_recommendations",
"edge_communication_events"
]
},
{
"chapter_id": "ch-006",
"chapter_name": "Calendar and Follow-Up",
"subchapter_id": "sub-006-04",
"subchapter_name": "Reminder Cards",
"component_types": [
"compact_alert_card",
"kanban_board",
"matrix_grid",
"stacked_reminder_cards",
"summary_strip"
],
"accepted_shapes": [
"insight_recommendation",
"reminder_priority_matrix",
"reminder_snooze_board",
"reminder_stack",
"reminder_strip"
],
"surface_targets": [
"webos",
"ipad",
"android_tablet",
"iphone_edge",
"android_phone_edge"
],
"policy_tags": [
"backend_owned",
"live_data_first",
"no_mock_fallback",
"surface_safe",
"bounded_actions",
"surface_agnostic"
],
"route_family": "mobile-edge",
"primary_tables": [
"user_calendar_events",
"insight_recommendations",
"edge_communication_events"
]
}
]

View File

@@ -0,0 +1,38 @@
{
"status": "ok",
"checks": {
"total_examples": 1200,
"unique_example_ids": 1200,
"subchapters_with_50_examples": true,
"chapters": 6,
"subchapters": 24,
"source_seed_examples_reported": 36,
"source_seed_examples_actual": 8
},
"per_subchapter": {
"sub-001-01": 50,
"sub-001-02": 50,
"sub-001-03": 50,
"sub-001-04": 50,
"sub-002-01": 50,
"sub-002-02": 50,
"sub-002-03": 50,
"sub-002-04": 50,
"sub-003-01": 50,
"sub-003-02": 50,
"sub-003-03": 50,
"sub-003-04": 50,
"sub-004-01": 50,
"sub-004-02": 50,
"sub-004-03": 50,
"sub-004-04": 50,
"sub-005-01": 50,
"sub-005-02": 50,
"sub-005-03": 50,
"sub-005-04": 50,
"sub-006-01": 50,
"sub-006-02": 50,
"sub-006-03": 50,
"sub-006-04": 50
}
}

BIN
.oracle_deploy_stage.tar Normal file

Binary file not shown.

View File

@@ -0,0 +1,167 @@
/**
* ComponentRegistry — maps CanvasComponent.type to renderer implementations.
* Supports lazy loading for expensive renderers (GeoMap, Table, PipelineBoard).
* Falls back to ErrorNoticeRenderer for unknown or revoked types.
*/
import { lazy, Suspense } from 'react';
import type { CanvasComponent } from '../types/canvas';
// ── Eager renderers (lightweight) ─────────────────────────────────────────────
import { KpiTileRenderer } from './renderers/KpiTileRenderer';
import { ErrorNoticeRenderer } from './renderers/ErrorNoticeRenderer';
import { TimelineRenderer } from './renderers/TimelineRenderer';
import { TextCanvasRenderer } from './renderers/TextCanvasRenderer';
// ── Lazy renderers (heavier) ──────────────────────────────────────────────────
const BarChartRenderer = lazy(() => import('./renderers/BarChartRenderer').then((m) => ({ default: m.BarChartRenderer })));
const LineChartRenderer = lazy(() => import('./renderers/LineChartRenderer').then((m) => ({ default: m.LineChartRenderer })));
const GeoMapRenderer = lazy(() => import('./renderers/GeoMapRenderer').then((m) => ({ default: m.GeoMapRenderer })));
const TableRenderer = lazy(() => import('./renderers/TableRenderer').then((m) => ({ default: m.TableRenderer })));
const PipelineBoardRenderer = lazy(() => import('./renderers/PipelineBoardRenderer').then((m) => ({ default: m.PipelineBoardRenderer })));
const ActivityStreamRenderer = lazy(() => import('./renderers/ActivityStreamRenderer').then((m) => ({ default: m.ActivityStreamRenderer })));
// ── Render context ────────────────────────────────────────────────────────────
export interface ComponentRenderContext {
tenantId: string;
actorRole: string;
showLineageBadges: boolean;
density: 'compact' | 'comfortable';
isSelected?: boolean;
onSelect?: (componentId: string) => void;
}
// ── Skeleton ──────────────────────────────────────────────────────────────────
function ComponentSkeleton({ variant }: { variant: string }) {
const heights: Record<string, number> = {
chart: 280, map: 380, table: 300, kpi: 120, pipeline: 360, timeline: 300, generic: 240,
};
const h = heights[variant] ?? 240;
return (
<div
className="rounded-2xl animate-pulse"
style={{
height: h,
background: 'rgba(255,255,255,0.03)',
border: '1px solid rgba(255,255,255,0.06)',
}}
/>
);
}
// ── Registry resolver ─────────────────────────────────────────────────────────
interface RegistryRendererProps {
component: CanvasComponent;
ctx: ComponentRenderContext;
}
export function ComponentRegistry({ component, ctx }: RegistryRendererProps) {
const skeleton = <ComponentSkeleton variant={component.renderingHints.skeletonVariant} />;
if (component.lifecycleState === 'revoked') {
return (
<ErrorNoticeRenderer
component={{
...component,
title: 'Component Revoked',
visualizationParameters: {
errorCode: 'component_revoked',
message: 'This component has been revoked and can no longer be rendered.',
},
}}
ctx={ctx}
/>
);
}
switch (component.type) {
case 'textCanvas':
return <TextCanvasRenderer component={component} ctx={ctx} />;
case 'kpiTile':
return <KpiTileRenderer component={component} ctx={ctx} />;
case 'errorNotice':
return <ErrorNoticeRenderer component={component} ctx={ctx} />;
case 'timeline':
return <TimelineRenderer component={component} ctx={ctx} />;
case 'barChart':
return (
<Suspense fallback={skeleton}>
<BarChartRenderer component={component} ctx={ctx} />
</Suspense>
);
case 'lineChart':
case 'forecastChart':
return (
<Suspense fallback={skeleton}>
<LineChartRenderer component={component} ctx={ctx} />
</Suspense>
);
case 'geoMap':
case 'heatmap':
return (
<Suspense fallback={skeleton}>
<GeoMapRenderer component={component} ctx={ctx} />
</Suspense>
);
case 'table':
return (
<Suspense fallback={skeleton}>
<TableRenderer component={component} ctx={ctx} />
</Suspense>
);
case 'pipelineBoard':
return (
<Suspense fallback={skeleton}>
<PipelineBoardRenderer component={component} ctx={ctx} />
</Suspense>
);
case 'activityStream':
return (
<Suspense fallback={skeleton}>
<ActivityStreamRenderer component={component} ctx={ctx} />
</Suspense>
);
case 'scatterPlot':
case 'customMLVisualization':
// Phase 2 renderers — show a meaningful placeholder with the right visual treatment
return (
<ErrorNoticeRenderer
component={{
...component,
visualizationParameters: {
errorCode: 'renderer_pending',
message: `The ${component.type} renderer is scheduled for Phase 2 synthesis. Data has been captured and is available.`,
severity: 'info',
},
}}
ctx={ctx}
/>
);
default:
return (
<ErrorNoticeRenderer
component={{
...component,
visualizationParameters: {
errorCode: 'unknown_type',
message: `Unknown component type: ${String(component.type)}`,
},
}}
ctx={ctx}
/>
);
}
}

View File

@@ -0,0 +1,35 @@
import type { CanvasComponent } from '../../types/canvas';
import { RendererWrapper, type ComponentRenderContext } from './RendererWrapper';
interface Props {
component: CanvasComponent;
ctx: ComponentRenderContext;
}
export function TextCanvasRenderer({ component, ctx }: Props) {
const params = component.visualizationParameters as {
content?: string;
};
const content = String(params.content ?? '').trim();
const paragraphs = content
.split(/\n{2,}/)
.map((block) => block.trim())
.filter(Boolean);
return (
<RendererWrapper component={component} ctx={ctx} minHeight={180}>
<div className="flex h-full flex-col gap-3 text-sm leading-7 text-zinc-200">
{paragraphs.length ? (
paragraphs.map((paragraph, index) => (
<p key={`${component.componentId}-${index}`} className="whitespace-pre-wrap text-zinc-300">
{paragraph}
</p>
))
) : (
<p className="text-zinc-500">No planning notes were generated for this prompt.</p>
)}
</div>
</RendererWrapper>
);
}

View File

@@ -0,0 +1,489 @@
/**
* Oracle Canvas — Canonical TypeScript Contracts
* Mirrors the JSON Schema from Section 6.2 of the Oracle Architecture Document v1.0
* These types replace the temporary OracleQueryResult contract.
*/
// ── Enums ─────────────────────────────────────────────────────────────────────
export type OracleRole =
| 'junior_broker'
| 'senior_broker'
| 'sales_director'
| 'marketing_operator'
| 'data_steward'
| 'compliance_reviewer'
| 'platform_admin';
export type ComponentType =
| 'textCanvas'
| 'kpiTile'
| 'barChart'
| 'lineChart'
| 'scatterPlot'
| 'geoMap'
| 'table'
| 'pipelineBoard'
| 'timeline'
| 'heatmap'
| 'forecastChart'
| 'activityStream'
| 'customMLVisualization'
| 'errorNotice';
export type ComponentLifecycleState = 'draft' | 'active' | 'superseded' | 'archived' | 'revoked';
export type PrivacyTier = 'standard' | 'restricted' | 'sensitive';
export type SourceType = 'postgres' | 'warehouse' | 'api' | 'materialized_view' | 'derived_dataset' | 'inline';
export type CachePolicyMode = 'none' | 'ttl' | 'revision_scoped';
export type IntentClass = 'analytical' | 'operational' | 'mixed';
export type ExecutionStatus =
| 'received'
| 'planning'
| 'validated'
| 'executing'
| 'completed'
| 'failed'
| 'clarification_required';
export type PageType = 'main' | 'fork';
export type ForkStatus = 'active' | 'merged' | 'closed';
export type MergeRequestStatus = 'open' | 'changes_requested' | 'approved' | 'merged' | 'closed';
export type TemplateStatus = 'catalog_active' | 'tenant_draft' | 'tenant_active' | 'archived' | 'revoked';
export type TemplateOrigin = 'premade' | 'synthesized' | 'cloned';
export type ShareMode = 'private' | 'direct_fork_only';
export type WidthMode = 'full' | 'half' | 'third';
export type VisibilityScope = 'private' | 'shared_fork' | 'tenant_team';
export type ComponentOriginType = 'catalog' | 'prompt_generated' | 'cloned' | 'merged' | 'edited';
export type PlacementMode =
| 'append_after_last_visible_component'
| 'insert_after_component'
| 'replace_component'
| 'group_under_section';
export type ActorType = 'user' | 'service' | 'ai';
export type LineageSourceKind =
| 'table'
| 'view'
| 'materialization'
| 'prompt'
| 'component'
| 'template'
| 'merge_request';
export type ValidationStatus = 'validated' | 'rejected' | 'needs_review';
export type CommitKind = 'prompt' | 'merge' | 'rollback' | 'manual_edit';
// ── Sub-objects ───────────────────────────────────────────────────────────────
export interface CachePolicy {
mode: CachePolicyMode;
ttlSeconds?: number;
}
export interface DataSourceDescriptor {
descriptorId: string;
sourceType: SourceType;
connectorId: string;
dataset: string;
authContextRef: string;
queryTemplate: string;
queryParameters: Record<string, unknown>;
rowLimit: number;
freshnessSlaSeconds?: number;
cachePolicy?: CachePolicy;
privacyTier: PrivacyTier;
lineageRefs?: string[];
}
export interface DataBindings {
dimensions: string[];
measures: string[];
series: string[];
filters: Array<{
field: string;
operator: string;
value: unknown;
}>;
}
export interface ComponentProvenance {
originType: ComponentOriginType;
templateId?: string;
promptExecutionId?: string;
sourceComponentId?: string;
sourceBranchId?: string;
mergeRequestId?: string;
createdBy: string;
createdAt: string;
}
export interface RenderingHints {
estimatedHeightPx: number;
skeletonVariant: 'chart' | 'map' | 'table' | 'kpi' | 'pipeline' | 'timeline' | 'generic';
virtualizationPriority: number;
}
export interface ComponentLayout {
orderIndex: number;
sectionId: string;
widthMode: WidthMode;
minHeightPx: number;
stickyHeader: boolean;
}
export interface AccessControls {
visibilityScope: VisibilityScope;
allowedRoles: OracleRole[];
redactionPolicy: string;
}
export interface StyleSignature {
theme: string;
paletteToken: string;
motionProfile: string;
density: 'compact' | 'comfortable';
radiusScale: string;
typographyScale: string;
}
export interface ValidationState {
schema: 'pass' | 'fail';
policy: 'pass' | 'fail';
a11y: 'pass' | 'fail';
performance: 'pass' | 'fail';
status: ValidationStatus;
}
// ── Core Entities ─────────────────────────────────────────────────────────────
export interface CanvasComponent {
componentId: string;
type: ComponentType;
title: string;
description?: string;
dataSourceDescriptor: DataSourceDescriptor;
visualizationParameters: Record<string, unknown>;
dataBindings: DataBindings;
version: number;
lifecycleState?: ComponentLifecycleState;
provenance: ComponentProvenance;
renderingHints: RenderingHints;
layout: ComponentLayout;
accessControls: AccessControls;
styleSignature: StyleSignature;
validationState: ValidationState;
auditLog: string[];
// Runtime-only: actual data rows fetched for this component
dataRows?: Record<string, unknown>[];
}
export interface ForkRecord {
forkId: string;
sourcePageId: string;
sourceBranchId: string;
sourceRevision: number;
forkPageId: string;
forkBranchId: string;
recipientUserId: string;
createdBy: string;
createdAt: string;
status: ForkStatus;
}
export interface LineageRecord {
lineageRecordId: string;
tenantId: string;
sourceKind: LineageSourceKind;
sourceId: string;
transformationType: string;
transformationSpecHash?: string;
producedKind: string;
producedId: string;
policySnapshotId?: string;
createdAt: string;
}
export interface SharingPolicy {
shareMode: ShareMode;
allowReshare: boolean;
defaultForkVisibility: 'private' | 'team';
}
export interface PagePresence {
activeViewers: number;
activeEditors: number;
lastPresenceAt: string;
}
export interface PageAuditSummary {
lastAuditEventId: string;
eventCount: number;
}
export interface CanvasPage {
pageId: string;
tenantId: string;
ownerId: string;
branchId: string;
branchName: string;
pageType: PageType;
title: string;
createdAt: string;
updatedAt: string;
isShared: boolean;
forks: ForkRecord[];
mainBranchPointer: {
pageId: string;
branchId: string;
revision: number;
};
baseRevision: number;
headRevision: number;
sharingPolicy: SharingPolicy;
presence: PagePresence;
lineage: LineageRecord[];
audit: PageAuditSummary;
components: CanvasComponent[];
}
export interface PromptExecution {
executionId: string;
tenantId: string;
pageId: string;
branchId: string;
actorId: string;
prompt: string;
intentClass: IntentClass;
status: ExecutionStatus;
modelRuntime: string;
semanticModelVersion: string;
retrievalPlan?: Record<string, unknown>;
visualizationPlan?: Record<string, unknown>;
warnings: string[];
summary?: string;
componentsCreated?: string[];
createdAt: string;
completedAt?: string;
}
export interface ComponentTemplate {
templateId: string;
tenantId: string;
name: string;
category: string;
status: TemplateStatus;
origin: TemplateOrigin;
version: string;
acceptedShapes: string[];
styleSignature?: StyleSignature;
validationState?: ValidationState;
provenance?: ComponentProvenance;
createdAt: string;
updatedAt: string;
}
export interface ConflictRecord {
conflictId: string;
conflictClass:
| 'component_content_conflict'
| 'query_descriptor_conflict'
| 'layout_slot_conflict'
| 'access_policy_conflict'
| 'delete_edit_conflict'
| 'safe_append'
| 'safe_reorder';
componentId: string;
field?: string;
sourceValue?: unknown;
targetValue?: unknown;
description: string;
}
export interface DiffSummary {
componentsAdded: number;
componentsEdited: number;
componentsReordered: number;
componentsDeleted: number;
}
export interface MergeRequest {
mergeRequestId: string;
tenantId: string;
sourcePageId: string;
sourceBranchId: string;
sourceHeadRevision: number;
targetPageId: string;
targetBranchId: string;
targetBaseRevision: number;
title: string;
description?: string;
status: MergeRequestStatus;
conflicts: ConflictRecord[];
diffSummary?: DiffSummary;
createdBy: string;
reviewedBy?: string;
createdAt: string;
updatedAt: string;
}
export interface AuditEvent {
auditEventId: string;
tenantId: string;
entityType: string;
entityId: string;
action: string;
actorId: string;
actorType: ActorType;
correlationId: string;
executionId?: string;
createdAt: string;
details: Record<string, unknown>;
}
export interface UserProfile {
userId: string;
tenantId: string;
email: string;
displayName: string;
role: OracleRole;
timezone: string;
locale: string;
defaultPageId: string;
canvasPreferences: {
defaultDensity: 'compact' | 'comfortable';
defaultPlacementMode: PlacementMode;
showLineageBadges: boolean;
};
policyProfileId: string;
createdAt: string;
updatedAt: string;
}
// ── API Request/Response contracts ────────────────────────────────────────────
export interface PromptSubmitRequest {
clientRequestId: string;
branchId: string;
prompt: string;
conversationContext?: Array<{ role: 'user' | 'assistant'; content: string }>;
placementMode?: PlacementMode;
}
export interface PromptSubmitResponse {
executionId: string;
status: ExecutionStatus;
pageId: string;
branchId: string;
headRevision: number;
componentsCreated: string[];
components: CanvasComponent[];
summary: string;
warnings: string[];
}
export interface CanvasPageRevision {
revisionId: string;
pageId: string;
tenantId: string;
revisionNumber: number;
commitKind: CommitKind;
commitSummary?: string;
actorId: string;
executionId?: string;
mergeRequestId?: string;
createdAt: string;
}
export interface ForkCreateRequest {
recipientUserId: string;
sourceRevision: number;
visibility: 'private' | 'team';
message?: string;
}
export interface ForkCreateResponse {
forkId: string;
forkPageId: string;
forkBranchId: string;
status: ForkStatus;
sourceRevision: number;
}
export interface MergeRequestCreateRequest {
sourcePageId: string;
sourceBranchId: string;
targetPageId: string;
targetBranchId: string;
title: string;
description?: string;
}
export interface MergeReviewRequest {
decision: 'approve' | 'reject' | 'changes_requested';
comment?: string;
resolutions?: Array<{
conflictId: string;
resolutionType: 'source_wins' | 'target_wins' | 'manual_composite';
resolvedPayloadHash?: string;
comment?: string;
}>;
}
// ── WebSocket event types ─────────────────────────────────────────────────────
export type OracleWSEventType =
| 'oracle.prompt.received'
| 'oracle.prompt.validated'
| 'oracle.prompt.failed'
| 'oracle.page.revision.committed'
| 'oracle.page.rollback.committed'
| 'oracle.fork.created'
| 'oracle.merge_request.opened'
| 'oracle.merge_request.updated'
| 'oracle.merge_request.merged'
| 'oracle.component.template.promoted'
| 'oracle.presence.updated';
export interface OracleWSMessage {
type: OracleWSEventType;
tenantId: string;
pageId?: string;
branchId?: string;
correlationId: string;
timestamp: string;
payload: Record<string, unknown>;
}
// ── API Error envelope ────────────────────────────────────────────────────────
export interface OracleAPIError {
error: {
code: string;
message: string;
retryable: boolean;
correlationId: string;
details?: Record<string, unknown>;
};
}
export interface OracleEnvelope<T> {
status: 'ok';
data: T;
meta?: Record<string, unknown>;
}

View File

@@ -0,0 +1,106 @@
from __future__ import annotations
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel, Field
from backend.oracle.action_service import oracle_action_service
from backend.oracle.persona_service import persona_service
from backend.services.mcp_registry import mcp_registry
from backend.services.nemoclaw_runtime import nemoclaw_runtime
from backend.services.runtime_llm_service import runtime_llm_service
router = APIRouter()
class WorkflowPreviewRequest(BaseModel):
prompt: str = Field(..., min_length=1, max_length=4096)
tenant_id: str = "tenant_velocity"
actor_role: str = "sales_director"
class MCPExecuteRequest(BaseModel):
tool_name: str = Field(..., min_length=1, max_length=128)
query: str = Field(..., min_length=1, max_length=1024)
class OracleWritebackRequest(BaseModel):
action_id: str
tenant_id: str = "tenant_velocity"
actor_id: str = "oracle_operator"
target_entity_type: str = Field(..., min_length=1, max_length=64)
target_entity_id: str = Field(..., min_length=1, max_length=128)
action_type: str = Field(default="lead_writeback", min_length=1, max_length=128)
writeback_payload: dict = Field(default_factory=dict)
@router.get("/health")
async def oracle_health() -> dict:
return {
"status": "ok",
"persona": await persona_service.health(),
"mcp_tools": mcp_registry.list_tools(),
"runtime_llm": await runtime_llm_service.list_providers(),
}
@router.get("/mcp/tools")
async def oracle_mcp_tools() -> dict:
return {"status": "ok", "data": mcp_registry.list_tools()}
@router.post("/mcp/execute")
async def oracle_mcp_execute(request: Request, payload: MCPExecuteRequest) -> dict:
pool = getattr(request.app.state, "db_pool", None)
result = await mcp_registry.execute(payload.tool_name, payload.query, crm_pool=pool)
return {"status": "ok", "data": result}
@router.post("/workflow/preview")
async def workflow_preview(payload: WorkflowPreviewRequest) -> dict:
persona_plan = await persona_service.plan_for_prompt(
prompt=payload.prompt,
tenant_id=payload.tenant_id,
actor_role=payload.actor_role,
)
return {
"status": "ok",
"data": {
"persona_plan": persona_plan,
"workflow": nemoclaw_runtime.build_workflow_dispatch(
prompt=payload.prompt,
tenant_id=payload.tenant_id,
actor_role=payload.actor_role,
component_templates=persona_plan["recommendedTemplates"],
),
},
}
@router.get("/actions")
async def list_oracle_actions(status: str | None = None, limit: int = 50) -> dict:
actions = await oracle_action_service.list_actions(status=status, limit=limit)
return {"status": "ok", "data": actions, "meta": {"count": len(actions)}}
@router.get("/actions/{action_id}")
async def get_oracle_action(action_id: str) -> dict:
action = await oracle_action_service.get_action(action_id)
if not action:
raise HTTPException(status_code=404, detail=f"Oracle action '{action_id}' not found.")
return {"status": "ok", "data": action}
@router.post("/actions/writeback")
async def apply_oracle_writeback(request: Request, payload: OracleWritebackRequest) -> dict:
result = await oracle_action_service.apply_writeback(payload.model_dump())
if hasattr(request.app.state, "broadcast_crm_event"):
await request.app.state.broadcast_crm_event(
{
"type": "oracle_writeback",
"entity": payload.target_entity_type,
"entity_id": payload.target_entity_id,
"action_id": payload.action_id,
"payload": result["resultPayload"],
}
)
return {"status": "ok", "data": result}

View File

@@ -0,0 +1,404 @@
"""
routes_oracle_templates.py
──────────────────────────
Oracle Template Catalog API
Extends the existing Oracle route surface with template taxonomy and seeding.
Endpoints:
GET /oracle/template-chapters — list chapters
POST /oracle/template-chapters — create a chapter
GET /oracle/template-subchapters — list subchapters (optionally filtered)
POST /oracle/template-subchapters — create a subchapter
GET /oracle/component-templates — list templates (filterable)
POST /oracle/component-templates — create a template
GET /oracle/component-templates/{id} — get a template
POST /oracle/component-templates/{id}/seed — add a seed example
GET /oracle/component-templates/{id}/seed — list seed examples for a template
POST /oracle/component-templates/synthetic-jobs — trigger a Kimi synthetic job
"""
from __future__ import annotations
import json
import logging
import os
from typing import Any, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from pydantic import BaseModel, Field
from backend.auth.dependencies import get_current_user
logger = logging.getLogger("velocity.oracle_templates")
router = APIRouter()
_DEFAULT_TENANT_ID = os.getenv("ORACLE_DEFAULT_TENANT_ID", "tenant_velocity")
# ── Helpers ───────────────────────────────────────────────────────────────────
def _pool(request: Request):
pool = request.app.state.db_pool
if pool is None:
raise HTTPException(503, "Database unavailable.")
return pool
def _tenant_id() -> str:
return _DEFAULT_TENANT_ID
# ── Models ────────────────────────────────────────────────────────────────────
class ChapterCreate(BaseModel):
name: str
description: Optional[str] = None
sort_order: int = 0
class SubchapterCreate(BaseModel):
chapter_id: str
name: str
description: Optional[str] = None
sort_order: int = 0
class TemplateCreate(BaseModel):
name: str
category: str
chapter_id: Optional[str] = None
subchapter_id: Optional[str] = None
component_type: Optional[str] = None
accepted_shapes: list[str] = Field(default_factory=list)
json_template: Optional[dict] = None
description: Optional[str] = None
origin: str = "premade"
version: str = "1.0.0"
class SeedExampleCreate(BaseModel):
title: str
example_json: dict
quality_notes: Optional[str] = None
chapter_id: Optional[str] = None
subchapter_id: Optional[str] = None
is_canonical: bool = False
class SyntheticJobCreate(BaseModel):
template_id: str
chapter_id: Optional[str] = None
subchapter_id: Optional[str] = None
model: str = "kimi"
requested_count: int = Field(10, ge=1, le=500)
# ── Template Chapters ─────────────────────────────────────────────────────────
@router.get("/template-chapters", summary="List Oracle template chapters")
async def list_template_chapters(
request: Request,
include_inactive: bool = Query(False),
user=Depends(get_current_user),
):
pool = _pool(request)
async with pool.acquire() as conn:
where = "WHERE ch.tenant_id=$1" + ("" if include_inactive else " AND ch.is_active=TRUE")
rows = await conn.fetch(
f"""
SELECT ch.chapter_id, ch.name, ch.description, ch.sort_order, ch.is_active,
COUNT(sub.subchapter_id) FILTER (WHERE sub.is_active=TRUE) as subchapter_count,
COUNT(t.template_id) as template_count
FROM oracle_template_chapters ch
LEFT JOIN oracle_template_subchapters sub ON sub.chapter_id = ch.chapter_id
LEFT JOIN oracle_component_templates t ON t.chapter_id = ch.chapter_id
AND t.status != 'archived'
{where}
GROUP BY ch.chapter_id
ORDER BY ch.sort_order ASC
""",
_tenant_id(),
)
return {"chapters": [dict(r) for r in rows]}
@router.post("/template-chapters", status_code=status.HTTP_201_CREATED,
summary="Create a template chapter")
async def create_template_chapter(
request: Request,
body: ChapterCreate,
user=Depends(get_current_user),
):
pool = _pool(request)
async with pool.acquire() as conn:
row = await conn.fetchrow(
"""
INSERT INTO oracle_template_chapters (tenant_id, name, description, sort_order)
VALUES ($1,$2,$3,$4)
RETURNING chapter_id, created_at
""",
_tenant_id(), body.name, body.description, body.sort_order,
)
return {"chapter_id": str(row["chapter_id"]), "created_at": str(row["created_at"])}
# ── Template Subchapters ──────────────────────────────────────────────────────
@router.get("/template-subchapters", summary="List Oracle template subchapters")
async def list_template_subchapters(
request: Request,
chapter_id: Optional[str] = Query(None),
include_inactive: bool = Query(False),
user=Depends(get_current_user),
):
pool = _pool(request)
async with pool.acquire() as conn:
where = "WHERE sub.tenant_id=$1"
params: list[Any] = [_tenant_id()]
idx = 2
if not include_inactive:
where += " AND sub.is_active=TRUE"
if chapter_id:
where += f" AND sub.chapter_id=${idx}"; params.append(chapter_id); idx += 1
rows = await conn.fetch(
f"""
SELECT sub.subchapter_id, sub.chapter_id, ch.name as chapter_name,
sub.name, sub.description, sub.sort_order, sub.is_active,
COUNT(t.template_id) as template_count
FROM oracle_template_subchapters sub
JOIN oracle_template_chapters ch ON ch.chapter_id = sub.chapter_id
LEFT JOIN oracle_component_templates t ON t.subchapter_id = sub.subchapter_id
AND t.status != 'archived'
{where}
GROUP BY sub.subchapter_id, ch.name
ORDER BY sub.chapter_id, sub.sort_order ASC
""",
*params,
)
return {"subchapters": [dict(r) for r in rows]}
@router.post("/template-subchapters", status_code=status.HTTP_201_CREATED,
summary="Create a template subchapter")
async def create_template_subchapter(
request: Request,
body: SubchapterCreate,
user=Depends(get_current_user),
):
pool = _pool(request)
async with pool.acquire() as conn:
# Verify chapter exists and belongs to tenant
ch_exists = await conn.fetchval(
"SELECT 1 FROM oracle_template_chapters WHERE chapter_id=$1 AND tenant_id=$2",
body.chapter_id, _tenant_id(),
)
if not ch_exists:
raise HTTPException(404, "Chapter not found")
row = await conn.fetchrow(
"""
INSERT INTO oracle_template_subchapters
(chapter_id, tenant_id, name, description, sort_order)
VALUES ($1,$2,$3,$4,$5)
RETURNING subchapter_id, created_at
""",
body.chapter_id, _tenant_id(), body.name, body.description, body.sort_order,
)
return {"subchapter_id": str(row["subchapter_id"]), "created_at": str(row["created_at"])}
# ── Component Templates ───────────────────────────────────────────────────────
@router.get("/component-templates", summary="List Oracle component templates")
async def list_component_templates(
request: Request,
chapter_id: Optional[str] = Query(None),
subchapter_id: Optional[str] = Query(None),
status_filter: Optional[str] = Query(None, alias="status"),
search: Optional[str] = Query(None),
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
user=Depends(get_current_user),
):
pool = _pool(request)
where = "WHERE t.tenant_id=$1"
params: list[Any] = [_tenant_id()]
idx = 2
if chapter_id:
where += f" AND t.chapter_id=${idx}"; params.append(chapter_id); idx += 1
if subchapter_id:
where += f" AND t.subchapter_id=${idx}"; params.append(subchapter_id); idx += 1
if status_filter:
where += f" AND t.status=${idx}"; params.append(status_filter); idx += 1
if search:
where += f" AND (t.name ILIKE ${idx} OR t.description ILIKE ${idx})"
params.append(f"%{search}%"); idx += 1
async with pool.acquire() as conn:
rows = await conn.fetch(
f"""
SELECT t.template_id, t.name, t.category, t.status, t.origin, t.version,
t.accepted_shapes, t.use_count, t.chapter_id, t.subchapter_id,
t.description, ch.name as chapter_name, sub.name as subchapter_name,
t.created_at, t.updated_at
FROM oracle_component_templates t
LEFT JOIN oracle_template_chapters ch ON ch.chapter_id = t.chapter_id
LEFT JOIN oracle_template_subchapters sub ON sub.subchapter_id = t.subchapter_id
{where}
ORDER BY t.updated_at DESC
LIMIT ${idx} OFFSET ${idx+1}
""",
*params, limit, offset,
)
total = await conn.fetchval(
f"SELECT COUNT(*) FROM oracle_component_templates t {where}", *params,
)
return {"total": total, "limit": limit, "offset": offset, "templates": [dict(r) for r in rows]}
@router.post("/component-templates", status_code=status.HTTP_201_CREATED,
summary="Create a component template")
async def create_component_template(
request: Request,
body: TemplateCreate,
user=Depends(get_current_user),
):
pool = _pool(request)
async with pool.acquire() as conn:
row = await conn.fetchrow(
"""
INSERT INTO oracle_component_templates (
tenant_id, name, category, chapter_id, subchapter_id,
accepted_shapes, json_template, description, origin, version, status
) VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,$8,$9,$10,'draft')
RETURNING template_id, created_at
""",
_tenant_id(), body.name, body.category, body.chapter_id, body.subchapter_id,
body.accepted_shapes,
json.dumps(body.json_template) if body.json_template else None,
body.description, body.origin, body.version,
)
return {"template_id": str(row["template_id"]), "created_at": str(row["created_at"])}
@router.get("/component-templates/{template_id}", summary="Get a component template")
async def get_component_template(
template_id: str,
request: Request,
user=Depends(get_current_user),
):
pool = _pool(request)
async with pool.acquire() as conn:
row = await conn.fetchrow(
"""
SELECT t.*, ch.name as chapter_name, sub.name as subchapter_name
FROM oracle_component_templates t
LEFT JOIN oracle_template_chapters ch ON ch.chapter_id = t.chapter_id
LEFT JOIN oracle_template_subchapters sub ON sub.subchapter_id = t.subchapter_id
WHERE t.template_id=$1 AND t.tenant_id=$2
""",
template_id, _tenant_id(),
)
if not row:
raise HTTPException(404, "Template not found")
return dict(row)
# ── Seed Examples ─────────────────────────────────────────────────────────────
@router.post("/component-templates/{template_id}/seed", status_code=status.HTTP_201_CREATED,
summary="Add a seed example to a template")
async def add_seed_example(
template_id: str,
request: Request,
body: SeedExampleCreate,
user=Depends(get_current_user),
):
pool = _pool(request)
async with pool.acquire() as conn:
exists = await conn.fetchval(
"SELECT 1 FROM oracle_component_templates WHERE template_id=$1 AND tenant_id=$2",
template_id, _tenant_id(),
)
if not exists:
raise HTTPException(404, "Template not found")
row = await conn.fetchrow(
"""
INSERT INTO oracle_template_seed_examples (
template_id, chapter_id, subchapter_id, title, example_json,
quality_notes, is_canonical
) VALUES ($1,$2,$3,$4,$5::jsonb,$6,$7)
RETURNING example_id, created_at
""",
template_id, body.chapter_id, body.subchapter_id, body.title,
json.dumps(body.example_json), body.quality_notes, body.is_canonical,
)
return {"example_id": str(row["example_id"]), "created_at": str(row["created_at"])}
@router.get("/component-templates/{template_id}/seed", summary="List seed examples for a template")
async def list_seed_examples(
template_id: str,
request: Request,
user=Depends(get_current_user),
):
pool = _pool(request)
async with pool.acquire() as conn:
rows = await conn.fetch(
"""
SELECT example_id, title, example_json, quality_notes, is_canonical, created_at
FROM oracle_template_seed_examples
WHERE template_id=$1
ORDER BY is_canonical DESC, created_at ASC
""",
template_id,
)
return {"examples": [dict(r) for r in rows]}
# ── Synthetic Jobs ────────────────────────────────────────────────────────────
@router.post("/component-templates/synthetic-jobs", status_code=status.HTTP_201_CREATED,
summary="Trigger a Kimi synthetic data generation job")
async def trigger_synthetic_job(
request: Request,
body: SyntheticJobCreate,
user=Depends(get_current_user),
):
"""
Queues a Kimi synthetic data expansion job for a template.
The job will be picked up by the background synthetic generation worker.
"""
pool = _pool(request)
async with pool.acquire() as conn:
exists = await conn.fetchval(
"SELECT 1 FROM oracle_component_templates WHERE template_id=$1 AND tenant_id=$2",
body.template_id, _tenant_id(),
)
if not exists:
raise HTTPException(404, "Template not found")
row = await conn.fetchrow(
"""
INSERT INTO oracle_synthetic_generation_jobs (
tenant_id, template_id, chapter_id, subchapter_id,
model, requested_count, created_by
) VALUES ($1,$2,$3,$4,$5,$6,$7)
RETURNING job_id, status, created_at
""",
_tenant_id(), body.template_id, body.chapter_id, body.subchapter_id,
body.model, body.requested_count, user.user_id,
)
logger.info(
"Synthetic job queued: %s for template %s (%d examples)",
row["job_id"], body.template_id, body.requested_count,
)
return {
"job_id": str(row["job_id"]),
"status": row["status"],
"created_at": str(row["created_at"]),
}

View File

@@ -0,0 +1,140 @@
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Request, status
from pydantic import BaseModel, Field
from backend.auth.dependencies import UserPrincipal, get_current_user
from backend.services.runtime_llm_service import runtime_llm_service
router = APIRouter()
class ChatMessage(BaseModel):
role: str = Field(..., pattern="^(system|user|assistant)$")
content: str = Field(..., min_length=1)
class RuntimeChatRequest(BaseModel):
provider: str | None = None
model: str | None = None
system_prompt: str | None = None
messages: list[ChatMessage]
temperature: float = Field(default=0.2, ge=0.0, le=2.0)
response_format: str | None = Field(default=None, pattern="^(json|text)$")
metadata: dict[str, Any] = Field(default_factory=dict)
class BatchItemRequest(BaseModel):
request_id: str
messages: list[ChatMessage]
system_prompt: str | None = None
temperature: float = Field(default=0.2, ge=0.0, le=2.0)
response_format: str | None = Field(default=None, pattern="^(json|text)$")
metadata: dict[str, Any] = Field(default_factory=dict)
class RuntimeBatchRequest(BaseModel):
provider: str | None = None
model: str | None = None
job_type: str = Field(..., min_length=1, max_length=128)
metadata: dict[str, Any] = Field(default_factory=dict)
items: list[BatchItemRequest] = Field(..., min_length=1, max_length=128)
def _normalize_user(user: UserPrincipal) -> dict[str, str]:
return {
"user_id": user.user_id,
"role": user.role,
}
@router.get("/providers", summary="List configured runtime LLM providers and models")
async def list_runtime_providers(_: UserPrincipal = Depends(get_current_user)) -> dict:
return {"status": "ok", "data": await runtime_llm_service.list_providers()}
@router.post("/chat", summary="Execute a single runtime LLM chat completion")
async def runtime_chat(
payload: RuntimeChatRequest,
user: UserPrincipal = Depends(get_current_user),
) -> dict:
response = await runtime_llm_service.chat(
provider_id=payload.provider,
model=payload.model,
system_prompt=payload.system_prompt,
messages=[message.model_dump() for message in payload.messages],
temperature=payload.temperature,
response_format=payload.response_format,
metadata={
**payload.metadata,
"requested_by": _normalize_user(user),
},
)
return {"status": "ok", "data": response}
@router.post("/batch", status_code=status.HTTP_202_ACCEPTED, summary="Submit a persisted runtime LLM batch job")
async def runtime_batch(
payload: RuntimeBatchRequest,
request: Request,
user: UserPrincipal = Depends(get_current_user),
) -> dict:
pool = getattr(request.app.state, "db_pool", None)
result = await runtime_llm_service.submit_batch(
provider_id=payload.provider,
model=payload.model,
job_type=payload.job_type,
items=[item.model_dump() for item in payload.items],
metadata={
**payload.metadata,
"requested_by": _normalize_user(user),
},
pool=pool,
actor_id=user.user_id,
)
return {"status": "ok", "data": result}
@router.get("/jobs/{job_id}", summary="Get runtime LLM batch job status")
async def get_runtime_job(
job_id: str,
request: Request,
_: UserPrincipal = Depends(get_current_user),
) -> dict:
pool = getattr(request.app.state, "db_pool", None)
job = await runtime_llm_service.get_job(job_id, pool=pool)
if not job:
raise HTTPException(status_code=404, detail=f"Runtime LLM job '{job_id}' not found.")
return {
"status": "ok",
"data": {
"job_id": job["job_id"],
"status": job["status"],
"provider": job["provider"],
"model": job["model"],
"job_type": job["job_type"],
"submitted_count": job["submitted_count"],
"completed_count": job["completed_count"],
"failed_count": job["failed_count"],
"created_at": job["created_at"],
"started_at": job["started_at"],
"completed_at": job["completed_at"],
"metadata": job.get("metadata") or {},
},
}
@router.get("/jobs/{job_id}/results", summary="Get runtime LLM batch job item results")
async def get_runtime_job_results(
job_id: str,
request: Request,
_: UserPrincipal = Depends(get_current_user),
) -> dict:
pool = getattr(request.app.state, "db_pool", None)
results = await runtime_llm_service.list_job_results(job_id, pool=pool)
if results is None:
raise HTTPException(status_code=404, detail=f"Runtime LLM job '{job_id}' not found.")
return {"status": "ok", "data": results, "meta": {"count": len(results)}}

View File

@@ -0,0 +1,411 @@
"""
Velocity — Unified FastAPI Backend
Covers: Catalyst (Meta Marketing), Sentinel (QD Engine), Vault (Trackable Links), Auth
GPU partitioning on AWS:
- NemoClaw / Ollama → CUDA devices 0, 1 (enforced in nemoclaw.service systemd unit)
- ComfyUI / Wan 2.2 → CUDA devices 2, 3 (enforced in comfyui.service systemd unit)
"""
import os
import json
import asyncio
import logging
import re
from contextlib import asynccontextmanager
from datetime import UTC, datetime
from pathlib import Path
from typing import Set
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends, HTTPException, status, UploadFile, File
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from dotenv import load_dotenv
def _load_velocity_env() -> None:
repo_root = Path(__file__).resolve().parent.parent
backend_root = repo_root / "backend"
explicit_env = os.getenv("VELOCITY_ENV_FILE", "").strip()
candidate_paths = []
if explicit_env:
candidate_paths.append(Path(explicit_env))
candidate_paths.extend(
[
backend_root / ".env",
repo_root / ".env",
]
)
loaded_any = False
seen: set[Path] = set()
for candidate in candidate_paths:
resolved = candidate.resolve()
if resolved in seen or not candidate.exists():
continue
load_dotenv(candidate, override=not loaded_any)
loaded_any = True
seen.add(resolved)
if not loaded_any:
load_dotenv()
_load_velocity_env()
from backend.api.routes_catalyst import router as catalyst_router
from backend.api.routes_crm import crm_router, analytics_router
from backend.api.routes_oracle import router as oracle_helper_router
from backend.api.routes_mobile_edge import router as mobile_edge_router
from backend.api.routes_inventory import router as inventory_router
from backend.api.routes_admin_surface import router as admin_surface_router
from backend.api.routes_oracle_templates import router as oracle_templates_router
from backend.api.routes_crm_imports import router as crm_imports_router
from backend.api.routes_runtime_llm import router as runtime_llm_router
from backend.auth.dependencies import (
create_access_token, verify_password, get_current_user, UserPrincipal
)
from backend.db.pool import create_pool, close_pool
from backend.oracle.router_v1 import router as oracle_v1_router
from backend.routers.cctv import router as cctv_router
from backend.routers.scenes import router as scenes_router
from backend.routers.videos import router as videos_router
from backend.routers.vault import router as vault_router
from backend.routers.sentinel import router as sentinel_router, broadcast_sentinel_event
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("velocity.main")
# ── Lifespan: DB pool init / teardown ─────────────────────────────────────────
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
try:
app.state.db_pool = await create_pool()
logger.info("asyncpg pool created")
except Exception as exc:
logger.error("Failed to create DB pool: %s", exc)
app.state.db_pool = None
app.state.broadcast_sentinel_event = broadcast_sentinel_event
yield
# Shutdown
await close_pool()
logger.info("asyncpg pool closed")
# ── App ───────────────────────────────────────────────────────────────────────
app = FastAPI(
title="Velocity — Neural Core",
description="Unified backend: Catalyst, Sentinel QD Engine, Vault, Oracle, Auth.",
version="2.0.0",
lifespan=lifespan,
)
# ── CORS ──────────────────────────────────────────────────────────────────────
origins = [o.strip() for o in os.getenv("CORS_ORIGINS", "http://localhost:5173").split(",")]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ── Static asset serving (Vault files) ───────────────────────────────────────
ASSET_DIR = os.getenv("VELOCITY_ASSET_DIR", "/opt/dlami/nvme/assets")
if os.path.isdir(ASSET_DIR):
app.mount("/assets", StaticFiles(directory=ASSET_DIR), name="assets")
def _sanitize_filename(value: str) -> str:
cleaned = re.sub(r"[^A-Za-z0-9._-]+", "_", value).strip("._")
return cleaned or "upload"
# ── Routers ───────────────────────────────────────────────────────────────────
app.include_router(catalyst_router, prefix="/api/catalyst", tags=["Catalyst"])
app.include_router(crm_router, prefix="/api", tags=["CRM"])
app.include_router(analytics_router, prefix="/api/analytics", tags=["Analytics"])
app.include_router(oracle_helper_router, prefix="/api/oracle", tags=["Oracle"])
app.include_router(oracle_v1_router, prefix="/api/oracle/v1", tags=["Oracle V1"])
app.include_router(oracle_templates_router, prefix="/api/oracle", tags=["Oracle Templates"])
app.include_router(sentinel_router, prefix="/api/sentinel", tags=["Sentinel"])
app.include_router(cctv_router, prefix="/api/cctv", tags=["CCTV"])
app.include_router(scenes_router, prefix="/api/scenes", tags=["Scenes"])
app.include_router(videos_router, prefix="/api/videos", tags=["Videos"])
app.include_router(vault_router, prefix="/api/vault", tags=["Vault"])
app.include_router(mobile_edge_router, prefix="/api/mobile-edge", tags=["Mobile Edge"])
app.include_router(inventory_router, prefix="/api/inventory", tags=["Inventory"])
app.include_router(admin_surface_router, prefix="/api/admin-surface", tags=["Admin Surface"])
app.include_router(crm_imports_router, prefix="/api", tags=["CRM Canonical"])
app.include_router(runtime_llm_router, prefix="/api/runtime/llm", tags=["Runtime LLM"])
# Public vault link (no /api prefix — shared externally with prospects)
from backend.routers.vault import router as public_vault_router
app.include_router(public_vault_router, prefix="/vault", tags=["Vault Public"])
# ── Auth endpoint ─────────────────────────────────────────────────────────────
from fastapi import HTTPException, status
from pydantic import BaseModel
class LoginRequest(BaseModel):
email: str
password: str
@app.post("/api/auth/login", tags=["Auth"])
async def login(body: LoginRequest):
"""
Authenticate a user and return a JWT.
Credentials are verified against the users_and_roles table.
"""
from backend.db.pool import get_pool
from fastapi import Request
pool = app.state.db_pool
if pool is None:
raise HTTPException(status_code=503, detail="Database unavailable.")
async with pool.acquire() as conn:
row = await conn.fetchrow(
"SELECT id::text, role, password_hash FROM users_and_roles WHERE email = $1 AND is_active = TRUE",
body.email,
)
if not row or not verify_password(body.password, row["password_hash"]):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid email or password.",
)
token = create_access_token(user_id=row["id"], role=row["role"])
return {"access_token": token, "token_type": "bearer", "expires_in": 28800}
@app.get("/api/auth/me", tags=["Auth"])
async def me(user: UserPrincipal = Depends(get_current_user)):
pool = app.state.db_pool
if pool is None:
raise HTTPException(status_code=503, detail="Database unavailable.")
async with pool.acquire() as conn:
row = await conn.fetchrow(
"""
SELECT full_name, email, avatar_url
FROM users_and_roles
WHERE id = $1::uuid
""",
user.user_id,
)
return {
"user_id": user.user_id,
"role": user.role,
"full_name": row["full_name"] if row else None,
"email": row["email"] if row else None,
"avatar_url": row["avatar_url"] if row else None,
}
@app.get("/api/auth/users", tags=["Auth"])
async def list_auth_users(_: UserPrincipal = Depends(get_current_user)):
pool = app.state.db_pool
if pool is None:
raise HTTPException(status_code=503, detail="Database unavailable.")
async with pool.acquire() as conn:
rows = await conn.fetch(
"""
SELECT
id::text AS user_id,
role,
full_name,
email,
avatar_url
FROM users_and_roles
WHERE is_active = TRUE
ORDER BY
COALESCE(NULLIF(full_name, ''), email, id::text) ASC
"""
)
return [
{
"user_id": row["user_id"],
"role": row["role"],
"full_name": row["full_name"],
"email": row["email"],
"avatar_url": row["avatar_url"],
}
for row in rows
]
@app.post("/api/auth/profile/avatar", tags=["Auth"])
async def upload_profile_avatar(
file: UploadFile = File(...),
user: UserPrincipal = Depends(get_current_user),
):
pool = app.state.db_pool
if pool is None:
raise HTTPException(status_code=503, detail="Database unavailable.")
allowed = {"image/png", "image/jpeg", "image/jpg", "image/webp"}
if file.content_type not in allowed:
raise HTTPException(status_code=400, detail="Unsupported avatar format.")
extension = Path(file.filename or "avatar.png").suffix.lower() or ".png"
if extension not in {".png", ".jpg", ".jpeg", ".webp"}:
extension = ".png"
avatar_dir = Path(ASSET_DIR) / "profile_avatars"
avatar_dir.mkdir(parents=True, exist_ok=True)
filename = f"{user.user_id}_{_sanitize_filename(Path(file.filename or 'avatar').stem)}_{int(datetime.now(UTC).timestamp())}{extension}"
destination = avatar_dir / filename
contents = await file.read()
destination.write_bytes(contents)
avatar_url = f"/assets/profile_avatars/{filename}"
async with pool.acquire() as conn:
await conn.execute(
"""
UPDATE users_and_roles
SET avatar_url = $2
WHERE id = $1::uuid
""",
user.user_id,
avatar_url,
)
return {"avatar_url": avatar_url}
# ── Catalyst WebSocket (preserved from v1) ────────────────────────────────────
class _CatalystManager:
def __init__(self) -> None:
self.active: Set[WebSocket] = set()
async def connect(self, ws: WebSocket) -> None:
await ws.accept()
self.active.add(ws)
def disconnect(self, ws: WebSocket) -> None:
self.active.discard(ws)
async def broadcast(self, payload: dict) -> None:
dead: Set[WebSocket] = set()
for ws in self.active:
try:
await ws.send_text(json.dumps(payload))
except Exception:
dead.add(ws)
self.active -= dead
_catalyst_mgr = _CatalystManager()
class _CRMManager:
def __init__(self) -> None:
self.active: Set[WebSocket] = set()
async def connect(self, ws: WebSocket) -> None:
await ws.accept()
self.active.add(ws)
def disconnect(self, ws: WebSocket) -> None:
self.active.discard(ws)
async def broadcast(self, payload: dict) -> None:
dead: Set[WebSocket] = set()
for ws in self.active:
try:
await ws.send_text(json.dumps(payload))
except Exception:
dead.add(ws)
self.active -= dead
_crm_mgr = _CRMManager()
@app.websocket("/ws/catalyst")
async def catalyst_ws(ws: WebSocket) -> None:
await _catalyst_mgr.connect(ws)
try:
while True:
data = await ws.receive_text()
await ws.send_text(json.dumps({"type": "ack", "data": data}))
except WebSocketDisconnect:
_catalyst_mgr.disconnect(ws)
@app.websocket("/ws/crm")
async def crm_ws(ws: WebSocket) -> None:
await _crm_mgr.connect(ws)
await _crm_mgr.broadcast(
{
"type": "crm_presence",
"connected_clients": len(_crm_mgr.active),
"timestamp": datetime.now(UTC).isoformat(),
}
)
try:
while True:
message = await ws.receive_text()
await ws.send_text(json.dumps({"type": "crm_ack", "data": message}))
except WebSocketDisconnect:
_crm_mgr.disconnect(ws)
async def broadcast_live_event(event_type, message, campaign_name=None, value=None):
payload = {
"type": event_type,
"message": message,
"campaignName": campaign_name,
"value": value,
"timestamp": datetime.now(UTC).isoformat(),
}
await _catalyst_mgr.broadcast(payload)
app.state.broadcast_live_event = broadcast_live_event
async def broadcast_crm_event(payload: dict) -> None:
enriched = {
**payload,
"timestamp": datetime.now(UTC).isoformat(),
}
await _crm_mgr.broadcast(enriched)
app.state.broadcast_crm_event = broadcast_crm_event
# ── Health ─────────────────────────────────────────────────────────────────────
@app.get("/health", tags=["Health"])
async def health() -> dict:
pool = app.state.db_pool
db_ok = pool is not None
return {
"status": "ok",
"service": "velocity-backend",
"version": "2.0.0",
"db_pool": "connected" if db_ok else "unavailable",
"timestamp": datetime.now(UTC).isoformat(),
}

View File

@@ -0,0 +1,340 @@
"""
oracle/codebook_service.py
Loads, normalizes, and retrieves Oracle Canvas codebook examples from the
expanded GPT and Claude seed packs delivered in Sprint 1.
The runtime treats the GPT pack as the primary normalized corpus and uses the
Claude pack as a supplement when it adds unique examples or metadata.
"""
from __future__ import annotations
import hashlib
import json
import logging
import re
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from typing import Any
logger = logging.getLogger(__name__)
_TOKEN_RE = re.compile(r"[a-z0-9]+")
_STOPWORDS = {
"a", "an", "and", "as", "at", "build", "canvas", "chart", "client", "clients",
"for", "from", "get", "give", "in", "into", "is", "list", "me", "of", "on",
"or", "oracle", "please", "render", "show", "surface", "that", "the", "this",
"to", "view", "with",
}
@dataclass(frozen=True)
class CodebookExample:
example_id: str
chapter_id: str
chapter_name: str
subchapter_id: str
subchapter_name: str
title: str
template_name: str
component_type: str
accepted_shapes: tuple[str, ...]
example_json: dict[str, Any]
quality_notes: str
is_canonical: bool
source_pack: str
surface_targets: tuple[str, ...]
policy_tags: tuple[str, ...]
backend_contract_hints: dict[str, Any]
score_terms: tuple[str, ...]
def _repo_root() -> Path:
return Path(__file__).resolve().parents[2]
def _safe_load_json(path: Path) -> dict[str, Any]:
with path.open("r", encoding="utf-8") as handle:
return json.load(handle)
def _tokenize(value: str) -> list[str]:
lowered = value.lower()
return [tok for tok in _TOKEN_RE.findall(lowered) if tok not in _STOPWORDS and len(tok) > 1]
def _make_template_id(example: dict[str, Any]) -> str:
base = "|".join(
[
example.get("chapter_id", ""),
example.get("subchapter_id", ""),
example.get("template_name", ""),
example.get("component_type", ""),
]
)
return hashlib.sha1(base.encode("utf-8")).hexdigest()[:16]
def _chapter_maps(payload: dict[str, Any]) -> tuple[dict[str, str], dict[str, str]]:
chapters: dict[str, str] = {}
subchapters: dict[str, str] = {}
for chapter in payload.get("chapters", []):
chapter_id = str(chapter.get("chapter_id", "")).strip()
if chapter_id:
chapters[chapter_id] = str(chapter.get("name", "")).strip()
for subchapter in chapter.get("subchapters", []):
sub_id = str(subchapter.get("subchapter_id", "")).strip()
if sub_id:
subchapters[sub_id] = str(subchapter.get("name", "")).strip()
return chapters, subchapters
def _normalize_examples(payload: dict[str, Any], source_pack: str) -> list[CodebookExample]:
chapter_names, subchapter_names = _chapter_maps(payload)
raw_examples = payload.get("seed_examples") or payload.get("examples") or []
normalized: list[CodebookExample] = []
for raw in raw_examples:
chapter_id = str(raw.get("chapter_id", "")).strip()
subchapter_id = str(raw.get("subchapter_id", "")).strip()
title = str(raw.get("title") or raw.get("template_name") or "Oracle Component").strip()
template_name = str(raw.get("template_name") or title).strip()
component_type = str(raw.get("component_type") or "summary_card").strip()
example_json = raw.get("example_json") or {}
terms = _tokenize(
" ".join(
[
title,
template_name,
component_type.replace("_", " "),
chapter_names.get(chapter_id, ""),
subchapter_names.get(subchapter_id, ""),
str(raw.get("quality_notes", "")),
" ".join(raw.get("policy_tags", []) or []),
]
)
)
normalized.append(
CodebookExample(
example_id=str(raw.get("example_id") or _make_template_id(raw)),
chapter_id=chapter_id,
chapter_name=chapter_names.get(chapter_id, chapter_id),
subchapter_id=subchapter_id,
subchapter_name=subchapter_names.get(subchapter_id, subchapter_id),
title=title,
template_name=template_name,
component_type=component_type,
accepted_shapes=tuple(raw.get("accepted_shapes") or []),
example_json=example_json,
quality_notes=str(raw.get("quality_notes") or ""),
is_canonical=bool(raw.get("is_canonical")),
source_pack=source_pack,
surface_targets=tuple(raw.get("surface_targets") or []),
policy_tags=tuple(raw.get("policy_tags") or []),
backend_contract_hints=dict(raw.get("backend_contract_hints") or {}),
score_terms=tuple(terms),
)
)
return normalized
class OracleCodebookService:
def __init__(self) -> None:
root = _repo_root()
self.runtime_merged_path = root / "backend" / "oracle" / "oracle_runtime_codebook_merged.json"
self.primary_path = root / ".Agent Context" / "Sprint 1" / "Sayan Multi-Surface and Oracle Delivery Pack" / "Sample JSON Schema" / "GPT 5.4" / "oracle_canvas_json_expansion_pack" / "db" / "oracle_template_seed_db_expanded_v1.pretty.json"
self.secondary_path = root / ".Agent Context" / "Sprint 1" / "Sayan Multi-Surface and Oracle Delivery Pack" / "Sample JSON Schema" / "Claude Sonnet 4.6" / "oracle_template_expansion" / "oracle_template_seed_db_expanded.json"
self.fallback_path = root / "backend" / "oracle" / "oracle_template_seed_db.json"
@lru_cache(maxsize=1)
def load(self) -> dict[str, Any]:
corpora: list[CodebookExample] = []
sources_loaded: list[str] = []
source_paths: list[tuple[Path, str]]
if self.runtime_merged_path.exists():
source_paths = [
(self.runtime_merged_path, "runtime_merged"),
(self.fallback_path, "runtime_seed_fallback"),
]
else:
source_paths = [
(self.primary_path, "gpt_5_4"),
(self.secondary_path, "claude_sonnet_4_6"),
(self.fallback_path, "runtime_seed_fallback"),
]
for path, label in source_paths:
if not path.exists():
continue
payload = _safe_load_json(path)
examples = _normalize_examples(payload, label)
if examples:
corpora.extend(examples)
sources_loaded.append(f"{label}:{len(examples)}")
deduped: dict[tuple[str, str, str], CodebookExample] = {}
for example in corpora:
key = (example.subchapter_id, example.template_name.lower(), example.title.lower())
existing = deduped.get(key)
if existing is None:
deduped[key] = example
continue
# Prefer canonical GPT examples, then canonical examples, then richer source pack.
if example.source_pack == "gpt_5_4" and existing.source_pack != "gpt_5_4":
deduped[key] = example
elif example.is_canonical and not existing.is_canonical:
deduped[key] = example
examples = list(deduped.values())
logger.info("Oracle codebook loaded from %s", ", ".join(sources_loaded) or "no sources")
return {
"examples": examples,
"source_summary": sources_loaded,
"template_count": len({(e.chapter_id, e.subchapter_id, e.template_name, e.component_type) for e in examples}),
}
def stats(self) -> dict[str, Any]:
data = self.load()
examples: list[CodebookExample] = data["examples"]
return {
"example_count": len(examples),
"template_count": data["template_count"],
"source_summary": data["source_summary"],
}
def list_templates(
self,
*,
category: str | None = None,
status: str | None = None,
search: str | None = None,
limit: int = 50,
offset: int = 0,
) -> dict[str, Any]:
del status # runtime codebook templates are always active catalog entries
examples: list[CodebookExample] = self.load()["examples"]
templates: dict[str, dict[str, Any]] = {}
for example in examples:
if category and category.lower() not in {example.chapter_name.lower(), example.subchapter_name.lower()}:
continue
if search:
terms = set(example.score_terms)
if not set(_tokenize(search)).intersection(terms):
continue
template_id = _make_template_id(
{
"chapter_id": example.chapter_id,
"subchapter_id": example.subchapter_id,
"template_name": example.template_name,
"component_type": example.component_type,
}
)
record = templates.get(template_id)
if record is None:
templates[template_id] = {
"templateId": template_id,
"tenantId": "_system",
"name": example.template_name,
"category": example.chapter_name,
"status": "catalog_active",
"origin": "premade",
"version": "codebook-v2",
"acceptedShapes": list(example.accepted_shapes),
"description": f"{example.subchapter_name} · {example.title}",
"chapterId": example.chapter_id,
"subchapterId": example.subchapter_id,
"componentType": example.component_type,
"sourcePack": example.source_pack,
"useCount": 0,
"updatedAt": None,
"createdAt": None,
}
ordered = list(templates.values())
ordered.sort(key=lambda item: (item["category"], item["name"]))
total = len(ordered)
return {
"total": total,
"templates": ordered[offset: offset + limit],
}
def search_examples(self, prompt: str, *, limit: int = 8) -> list[CodebookExample]:
prompt_terms = set(_tokenize(prompt))
if not prompt_terms:
prompt_terms = set(_tokenize(prompt.replace("_", " ")))
scored: list[tuple[int, CodebookExample]] = []
for example in self.load()["examples"]:
score = 0
term_set = set(example.score_terms)
overlap = prompt_terms.intersection(term_set)
score += len(overlap) * 6
lowered_prompt = prompt.lower()
if example.template_name.lower() in lowered_prompt:
score += 24
if example.subchapter_name.lower() in lowered_prompt:
score += 20
if example.chapter_name.lower() in lowered_prompt:
score += 14
if example.component_type.replace("_", " ") in lowered_prompt:
score += 12
if example.is_canonical:
score += 8
if "live_data_first" in example.policy_tags:
score += 4
if score > 0:
scored.append((score, example))
scored.sort(key=lambda item: (-item[0], item[1].chapter_id, item[1].subchapter_id, item[1].title))
selected: list[CodebookExample] = []
seen: set[tuple[str, str]] = set()
for _, example in scored:
dedupe_key = (example.subchapter_id, example.template_name)
if dedupe_key in seen:
continue
seen.add(dedupe_key)
selected.append(example)
if len(selected) >= limit:
break
return selected
def synthesize_template(self, prompt: str, data_shapes: list[str] | None = None) -> dict[str, Any]:
match = next(iter(self.search_examples(prompt, limit=1)), None)
shapes = data_shapes or []
if match is None:
return {
"templateId": hashlib.sha1(prompt.encode("utf-8")).hexdigest()[:16],
"tenantId": "_system",
"name": "Oracle Synthesized Draft",
"category": "Custom",
"status": "tenant_draft",
"origin": "synthesized",
"version": "1.0.0",
"acceptedShapes": shapes,
"description": f"Draft synthesized from prompt: {prompt[:120]}",
}
return {
"templateId": _make_template_id(
{
"chapter_id": match.chapter_id,
"subchapter_id": match.subchapter_id,
"template_name": match.template_name,
"component_type": match.component_type,
}
),
"tenantId": "_system",
"name": match.template_name,
"category": match.chapter_name,
"status": "catalog_active",
"origin": "premade",
"version": "codebook-v2",
"acceptedShapes": list(match.accepted_shapes or shapes),
"description": f"Best codebook match · {match.subchapter_name}",
"componentType": match.component_type,
"chapterId": match.chapter_id,
"subchapterId": match.subchapter_id,
"sourcePack": match.source_pack,
"exampleJson": match.example_json,
}
codebook_service = OracleCodebookService()

View File

@@ -0,0 +1,322 @@
"""
oracle/data_access_gateway.py
Read-only, policy-aware PostgreSQL query executor for Oracle datasets.
Nemoclaw is treated strictly as a planner. The gateway executes only
whitelisted dataset queries and always injects the actor's tenant scope.
"""
from __future__ import annotations
import logging
import os
from dataclasses import dataclass
from typing import Any
try:
import asyncpg # type: ignore
except Exception: # pragma: no cover
asyncpg = None # type: ignore
from .policy_service import PolicyContext, PolicyService
logger = logging.getLogger(__name__)
_DB_URL = os.getenv("DATABASE_URL", "")
_ALLOW_IN_MEMORY = os.getenv("ORACLE_ALLOW_IN_MEMORY_FALLBACK", "").lower() in {"1", "true", "yes"}
@dataclass
class QueryExecutionResult:
rows: list[dict[str, Any]]
warnings: list[str]
def _db_ready() -> bool:
return bool(_DB_URL and not _DB_URL.startswith("PLACEHOLDER") and asyncpg is not None)
class DataAccessGateway:
def __init__(self) -> None:
self.policy_service = PolicyService()
async def execute_component_plan(
self,
component_plan: dict[str, Any],
ctx: PolicyContext,
prompt: str,
) -> QueryExecutionResult:
dataset = str(component_plan.get("dataset", "")).strip()
if not dataset:
return QueryExecutionResult(rows=[], warnings=["Dataset missing from retrieval plan."])
validation = self.policy_service.validate_retrieval_plan(component_plan, ctx)
self.policy_service.audit_policy_check(ctx, dataset, validation)
if not validation.passed:
return QueryExecutionResult(rows=[], warnings=validation.errors)
if not _db_ready():
if _ALLOW_IN_MEMORY or "PYTEST_CURRENT_TEST" in os.environ:
return QueryExecutionResult(rows=[], warnings=[])
raise RuntimeError("Oracle requires DATABASE_URL and asyncpg for real-time data access.")
try:
rows = await self._query_dataset(
dataset=dataset,
row_limit=validation.effective_row_limit,
ctx=ctx,
prompt=prompt,
)
except Exception as exc:
logger.warning("DATA_GATEWAY query_failed dataset=%s error=%s", dataset, exc)
return QueryExecutionResult(rows=[], warnings=[f"{dataset}: {exc}"])
redacted = self.policy_service.redact(rows, validation.redaction_policy)
return QueryExecutionResult(rows=redacted, warnings=validation.warnings)
async def _query_dataset(
self,
*,
dataset: str,
row_limit: int,
ctx: PolicyContext,
prompt: str,
) -> list[dict[str, Any]]:
sql, params = self._build_whitelisted_query(dataset, row_limit, ctx, prompt)
assert asyncpg is not None
conn = await asyncpg.connect(_DB_URL)
try:
records = await conn.fetch(sql, *params)
finally:
await conn.close()
return [dict(record) for record in records]
def _build_whitelisted_query(
self,
dataset: str,
row_limit: int,
ctx: PolicyContext,
prompt: str,
) -> tuple[str, list[Any]]:
lower_prompt = prompt.lower()
if dataset == "deals":
sql = """
SELECT
stage,
COUNT(*)::int AS count,
COALESCE(SUM(value), 0)::float AS value,
COALESCE(
json_agg(
json_build_object(
'id', lead_id,
'name', lead_name,
'company', company,
'value', value_label,
'avatar', avatar_url
)
ORDER BY value DESC NULLS LAST
) FILTER (WHERE lead_id IS NOT NULL),
'[]'::json
) AS leads
FROM deals
WHERE tenant_id = $1
GROUP BY stage
ORDER BY COALESCE(SUM(value), 0) DESC, stage ASC
LIMIT $2
"""
return sql, [ctx.tenant_id, row_limit]
if dataset == "lead_daily_snapshot":
sql = """
SELECT
source,
COALESCE(SUM(qd_weighted_score), 0)::float AS qd_weighted_volume
FROM lead_daily_snapshot
WHERE tenant_id = $1
GROUP BY source
ORDER BY qd_weighted_volume DESC, source ASC
LIMIT $2
"""
return sql, [ctx.tenant_id, row_limit]
if dataset == "lead_geo_interest_rollup":
sql = """
SELECT
district,
lat,
lng,
COALESCE(lead_count, 0)::int AS lead_count,
COALESCE(avg_qd_score, 0)::float AS avg_qd_score,
COALESCE(x, 0)::float AS x,
COALESCE(y, 0)::float AS y
FROM lead_geo_interest_rollup
WHERE tenant_id = $1
ORDER BY lead_count DESC, district ASC
LIMIT $2
"""
return sql, [ctx.tenant_id, row_limit]
if dataset == "broker_performance":
sql = """
SELECT
ROW_NUMBER() OVER (ORDER BY COALESCE(revenue_generated, 0) DESC, broker_name ASC)::int AS rank,
broker_name AS name,
deals_closed::int AS deals_closed,
COALESCE(revenue_generated, 0)::float AS revenue_generated,
avatar_url AS avatar
FROM broker_performance
WHERE tenant_id = $1
ORDER BY revenue_generated DESC, broker_name ASC
LIMIT $2
"""
return sql, [ctx.tenant_id, row_limit]
if dataset == "inventory_absorption":
sql = """
SELECT
period_label AS period,
COALESCE(absorption_rate, 0)::float AS absorption_rate,
COALESCE(target_rate, 0)::float AS target_rate
FROM inventory_absorption
WHERE tenant_id = $1
ORDER BY period_start ASC
LIMIT $2
"""
return sql, [ctx.tenant_id, row_limit]
if dataset == "oracle_aggregated_metric":
metric_name = "total_leads"
if "pipeline" in lower_prompt:
metric_name = "total_pipeline_value"
elif "quota" in lower_prompt or "attainment" in lower_prompt:
metric_name = "quota_attainment"
sql = """
SELECT
metric_value,
metric_label,
trend_value,
comparison_label
FROM oracle_aggregated_metric
WHERE tenant_id = $1
AND metric_name = $2
ORDER BY observed_at DESC
LIMIT 1
"""
return sql, [ctx.tenant_id, metric_name]
if dataset == "lead_activity_log":
if "follow-up" in lower_prompt or "queue" in lower_prompt:
sql = """
SELECT
lead_name AS name,
assigned_broker,
COALESCE(last_contact_hours_ago, 0)::int AS last_contact_hours_ago,
COALESCE(qd_score, 0)::float AS qd_score,
urgency,
avatar_url AS avatar
FROM lead_activity_log
WHERE tenant_id = $1
ORDER BY last_contact_hours_ago DESC, qd_score DESC
LIMIT $2
"""
return sql, [ctx.tenant_id, row_limit]
sql = """
SELECT
activity_type AS type,
COALESCE(activity_title, activity_summary, activity_type) AS title,
activity_summary AS summary,
actor_name AS actor,
TO_CHAR(activity_at, 'YYYY-MM-DD HH24:MI') AS date
FROM lead_activity_log
WHERE tenant_id = $1
ORDER BY activity_at DESC
LIMIT $2
"""
return sql, [ctx.tenant_id, row_limit]
if dataset == "crm_contacts_overview":
sql = """
SELECT
p.person_id::text AS id,
p.full_name AS name,
COALESCE(p.primary_email, '') AS email,
COALESCE(p.primary_phone, '') AS phone,
COALESCE(p.city, '') AS city,
COALESCE(p.buyer_type, 'unclassified') AS buyer_type,
COALESCE(q.qd_score, 0)::float AS qd_score
FROM crm_people p
LEFT JOIN LATERAL (
SELECT qd_score
FROM intel_qd_scores q
WHERE q.person_id = p.person_id
ORDER BY q.scored_at DESC
LIMIT 1
) q ON TRUE
ORDER BY qd_score DESC, p.full_name ASC
LIMIT $1
"""
return sql, [row_limit]
if dataset == "crm_opportunity_pipeline":
sql = """
SELECT
o.stage::text AS stage,
COUNT(*)::int AS count,
COALESCE(SUM(o.value), 0)::float AS value,
COALESCE(
json_agg(
json_build_object(
'id', o.opportunity_id,
'name', p.full_name,
'company', COALESCE(a.account_name, ''),
'value', COALESCE(o.value, 0),
'nextAction', COALESCE(o.next_action, '')
)
ORDER BY o.value DESC NULLS LAST
) FILTER (WHERE o.opportunity_id IS NOT NULL),
'[]'::json
) AS leads
FROM crm_opportunities o
JOIN crm_leads l ON l.lead_id = o.lead_id
JOIN crm_people p ON p.person_id = l.person_id
LEFT JOIN crm_accounts a ON a.account_id = l.account_id
GROUP BY o.stage
ORDER BY COALESCE(SUM(o.value), 0) DESC, o.stage::text ASC
LIMIT $1
"""
return sql, [row_limit]
if dataset == "crm_property_interest_rollup":
sql = """
SELECT
project_name AS category,
COUNT(*)::int AS value,
ROUND(AVG(COALESCE((budget_min + budget_max) / 2.0, budget_max, budget_min, 0)), 2)::float AS average_budget
FROM crm_property_interests
GROUP BY project_name
ORDER BY value DESC, project_name ASC
LIMIT $1
"""
return sql, [row_limit]
if dataset == "crm_interaction_timeline":
sql = """
SELECT
i.interaction_type AS type,
COALESCE(i.summary, i.interaction_type) AS title,
CONCAT(p.full_name, ' · ', i.channel::text) AS summary,
p.full_name AS actor,
TO_CHAR(i.happened_at, 'YYYY-MM-DD HH24:MI') AS date
FROM intel_interactions i
JOIN crm_people p ON p.person_id = i.person_id
ORDER BY i.happened_at DESC
LIMIT $1
"""
return sql, [row_limit]
raise ValueError(f"Dataset '{dataset}' is not whitelisted for Oracle execution.")
data_access_gateway = DataAccessGateway()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,876 @@
"""
oracle/prompt_orchestrator.py
Accepts a user prompt, assembles context, calls the Nemoclaw model runtime
(or uses a deterministic fallback), validates the generated plan via policy,
triggers the data access gateway, and produces a PromptExecution.
"""
from __future__ import annotations
import logging
import os
import uuid
import json
from datetime import datetime, timezone
from typing import Any
from .policy_service import PolicyContext, PolicyService
from .canvas_service import canvas_service
from .data_access_gateway import data_access_gateway
from .persona_service import persona_service
from .codebook_service import codebook_service, CodebookExample
from backend.services.runtime_llm_service import runtime_llm_service
from backend.services.nemoclaw_runtime import nemoclaw_runtime
try:
import asyncpg # type: ignore
except Exception: # pragma: no cover
asyncpg = None # type: ignore
logger = logging.getLogger(__name__)
_DB_URL = os.getenv("DATABASE_URL", "")
policy_svc = PolicyService()
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
# ── Execution store ───────────────────────────────────────────────────────────
_DEMO_EXECUTIONS: dict[str, dict[str, Any]] = {}
def _db_ready() -> bool:
return bool(_DB_URL and not _DB_URL.startswith("PLACEHOLDER") and asyncpg is not None)
# ── Semantic intent detection (simplified) ────────────────────────────────────
_INTENT_KEYWORDS: dict[str, list[str]] = {
"pipeline_board": ["pipeline", "stage", "kanban", "deal", "funnel"],
"bar_chart": ["bar", "compare", "source", "channel", "distribution", "ranked", "lead", "whale"],
"geo_map": ["map", "geographic", "location", "district", "region", "area", "dubai"],
"table": ["table", "list", "broker", "performance", "leaderboard", "rank", "top", "contact", "client", "account", "crm"],
"line_chart": ["trend", "time", "monthly", "weekly", "absorption", "forecast"],
"kpi_tile": ["kpi", "total", "summary", "attainment", "quota", "how many"],
"activity_stream": ["timeline", "activity", "history", "follow-up", "queue", "contact", "interaction", "message", "call", "email"],
}
def _detect_component_types(prompt: str) -> list[str]:
lower = prompt.lower()
types: list[str] = []
for comp_type, keywords in _INTENT_KEYWORDS.items():
if any(k in lower for k in keywords):
types.append(comp_type)
return types or ["bar_chart"]
def _build_demo_retrieval_plan(
prompt: str,
tenant_id: str,
actor_role: str,
) -> dict[str, Any]:
"""
Deterministic plan builder for demo mode.
Produces a valid retrieval plan that passes policy validation.
"""
component_types = _detect_component_types(prompt)
row_limit = 50 if actor_role in ("senior_broker", "junior_broker") else 200
return {
"planId": str(uuid.uuid4()),
"components": [
{
"suggestedType": ct,
"dataset": _DATASET_MAP.get(ct, "aggregated_results"),
"privacyTier": "standard",
"rowLimit": row_limit,
"joins": [],
"queryTemplate": f"SELECT * FROM {_DATASET_MAP.get(ct, 'aggregated_results')} WHERE tenant_id = :tenant_id LIMIT :limit",
"queryParameters": {"tenant_id": tenant_id, "limit": row_limit},
}
for ct in component_types
],
"semanticModelVersion": "oracle_semantic_v2026_04_08_01",
"intentClass": "analytical",
}
_DATASET_MAP: dict[str, str] = {
"pipeline_board": "deals",
"bar_chart": "lead_daily_snapshot",
"geo_map": "lead_geo_interest_rollup",
"table": "broker_performance",
"line_chart": "inventory_absorption",
"kpi_tile": "oracle_aggregated_metric",
"activity_stream": "lead_activity_log",
}
_CODEBOOK_COMPONENT_MAP: dict[str, str] = {
"summary_card": "kpi_tile",
"summary_strip": "kpi_tile",
"metric_card_group": "kpi_tile",
"compact_alert_card": "kpi_tile",
"gauge_stack": "kpi_tile",
"lead_profile_card": "table",
"property_card": "table",
"data_table": "table",
"leaderboard_table": "table",
"matrix_grid": "table",
"interaction_timeline": "activity_stream",
"message_thread_summary": "activity_stream",
"timeline": "activity_stream",
"bar_chart": "bar_chart",
"line_chart": "line_chart",
"heatmap": "geo_map",
"geo_map": "geo_map",
"pipeline_board": "pipeline_board",
}
def _component_plan_type_from_codebook(example: CodebookExample) -> str:
return _CODEBOOK_COMPONENT_MAP.get(example.component_type, "table")
def _dataset_for_codebook(example: CodebookExample, prompt: str, component_plan_type: str | None = None) -> str:
chapter = example.chapter_name.lower()
subchapter = example.subchapter_name.lower()
component_plan_type = component_plan_type or _component_plan_type_from_codebook(example)
lowered_prompt = prompt.lower()
if component_plan_type == "activity_stream":
return "crm_interaction_timeline"
if component_plan_type == "pipeline_board":
return "crm_opportunity_pipeline"
if component_plan_type == "line_chart" and any(term in lowered_prompt for term in ("trend", "time", "history", "growth")):
return "crm_property_interest_rollup"
if any(term in lowered_prompt for term in ("contact", "client 360", "crm", "account", "lead")):
if "timeline" in lowered_prompt or "message" in lowered_prompt or "call" in lowered_prompt or "email" in lowered_prompt:
return "crm_interaction_timeline"
if "pipeline" in lowered_prompt or "opportunit" in lowered_prompt:
return "crm_opportunity_pipeline"
if "interest" in lowered_prompt or "project" in lowered_prompt or "property" in lowered_prompt:
return "crm_property_interest_rollup"
return "crm_contacts_overview"
if "client" in chapter or "client" in subchapter or "contact" in subchapter:
return "crm_contacts_overview"
if "opportun" in chapter or "pipeline" in subchapter:
return "crm_opportunity_pipeline"
if "interaction" in chapter or "communication" in chapter or "timeline" in subchapter:
return "crm_interaction_timeline"
if "property" in chapter or "inventory" in chapter or "interest" in subchapter:
return "crm_property_interest_rollup"
return _DATASET_MAP.get(component_plan_type, "oracle_aggregated_metric")
def _build_codebook_retrieval_plan(
prompt: str,
tenant_id: str,
actor_role: str,
matches: list[CodebookExample],
) -> dict[str, Any]:
row_limit = 50 if actor_role in ("senior_broker", "junior_broker") else 200
desired_types = _detect_component_types(prompt)
if not desired_types:
desired_types = [_component_plan_type_from_codebook(matches[0])] if matches else ["table"]
title_hints: dict[str, str] = {}
for example in matches:
mapped = _component_plan_type_from_codebook(example)
title_hints.setdefault(mapped, example.title)
components: list[dict[str, Any]] = []
exemplar = matches[0]
for component_plan_type in desired_types[:4]:
dataset = _dataset_for_codebook(exemplar, prompt, component_plan_type)
components.append(
{
"suggestedType": component_plan_type,
"dataset": dataset,
"privacyTier": "standard",
"rowLimit": row_limit,
"joins": [],
"queryTemplate": f"SELECT * FROM {dataset} WHERE tenant_id = :tenant_id LIMIT :limit",
"queryParameters": {"tenant_id": tenant_id, "limit": row_limit},
"templateRef": {
"exampleId": exemplar.example_id,
"templateName": exemplar.template_name,
"componentType": exemplar.component_type,
"chapterName": exemplar.chapter_name,
"subchapterName": exemplar.subchapter_name,
"sourcePack": exemplar.source_pack,
},
"titleHint": title_hints.get(component_plan_type, exemplar.title),
}
)
return {
"planId": str(uuid.uuid4()),
"components": components,
"semanticModelVersion": "oracle_codebook_v2026_04_19_01",
"intentClass": "analytical",
"planner": "codebook_retrieval",
}
_RUNTIME_ALLOWED_DATASETS = {
"deals",
"lead_daily_snapshot",
"lead_geo_interest_rollup",
"broker_performance",
"inventory_absorption",
"oracle_aggregated_metric",
"lead_activity_log",
"crm_contacts_overview",
"crm_opportunity_pipeline",
"crm_property_interest_rollup",
"crm_interaction_timeline",
}
class PromptOrchestrator:
"""
Orchestrates the full prompt-to-canvas pipeline:
1. Intent classification
2. Retrieval plan construction (Nemoclaw or fallback)
3. Policy validation
4. Component plan construction
5. Execution record persistence
"""
async def execute(
self,
*,
tenant_id: str,
page_id: str,
branch_id: str,
actor_id: str,
actor_role: str,
prompt: str,
conversation_context: list[dict[str, str]] | None = None,
client_request_id: str,
placement_mode: str = "append_after_last_visible_component",
) -> dict[str, Any]:
"""
Full orchestration flow. Returns a PromptExecution dict.
"""
execution_id = str(uuid.uuid4())
now = _now()
warnings: list[str] = []
ctx = PolicyContext(
tenant_id=tenant_id,
actor_id=actor_id,
actor_role=actor_role,
)
execution: dict[str, Any] = {
"executionId": execution_id,
"tenantId": tenant_id,
"pageId": page_id,
"branchId": branch_id,
"actorId": actor_id,
"prompt": prompt,
"intentClass": "analytical",
"status": "planning",
"modelRuntime": "runtime_llm" if runtime_llm_service._provider_catalog() else "deterministic_fallback",
"semanticModelVersion": "oracle_semantic_v2026_04_08_01",
"warnings": warnings,
"componentsCreated": [],
"clientRequestId": client_request_id,
"createdAt": now,
"codebookMatches": [],
}
_DEMO_EXECUTIONS[execution_id] = execution
await self._persist_execution(execution)
# ── Step 1: Build retrieval plan ──────────────────────────────────────
codebook_matches = codebook_service.search_examples(prompt, limit=4)
execution["codebookMatches"] = [
{
"exampleId": match.example_id,
"templateName": match.template_name,
"componentType": match.component_type,
"chapterName": match.chapter_name,
"subchapterName": match.subchapter_name,
"sourcePack": match.source_pack,
}
for match in codebook_matches
]
if codebook_matches:
retrieval_plan = _build_codebook_retrieval_plan(prompt, tenant_id, actor_role, codebook_matches)
execution["status"] = "validated"
elif runtime_llm_service._provider_catalog():
try:
retrieval_plan = await self._call_nemoclaw(prompt, conversation_context or [], ctx)
execution["status"] = "validated"
except Exception as exc:
logger.warning("ORCH Nemoclaw call failed, using fallback: %s", exc)
warnings.append(f"Model runtime unavailable ({exc}); using deterministic fallback.")
retrieval_plan = _build_demo_retrieval_plan(prompt, tenant_id, actor_role)
else:
retrieval_plan = _build_demo_retrieval_plan(prompt, tenant_id, actor_role)
execution["retrievalPlan"] = retrieval_plan
persona_plan = await persona_service.plan_for_prompt(
prompt=prompt,
tenant_id=tenant_id,
actor_role=actor_role,
)
execution["personaPlan"] = persona_plan
execution["workflowDispatch"] = nemoclaw_runtime.build_workflow_dispatch(
prompt=prompt,
tenant_id=tenant_id,
actor_role=actor_role,
component_templates=persona_plan["recommendedTemplates"],
)
# ── Step 2: Policy validation ─────────────────────────────────────────
policy_errors = []
for component_plan in retrieval_plan.get("components", []):
result = policy_svc.validate_retrieval_plan(component_plan, ctx)
if not result.passed:
policy_errors.extend(result.errors)
if result.warnings:
warnings.extend(result.warnings)
if policy_errors:
execution["status"] = "failed"
execution["warnings"] = warnings + policy_errors
execution["completedAt"] = _now()
logger.warning(
"ORCH policy_denial execution_id=%s actor=%s errors=%s",
execution_id, actor_id, policy_errors,
)
return execution
execution["status"] = "executing"
await self._persist_execution(execution)
# ── Step 3: Build visualization plan (component descriptors) ──────────
viz_plan = await self._build_visualization_plan(
retrieval_plan=retrieval_plan,
prompt=prompt,
execution_id=execution_id,
actor_id=actor_id,
tenant_id=tenant_id,
branch_id=branch_id,
placement_mode=placement_mode,
ctx=ctx,
persona_plan=persona_plan,
)
execution["visualizationPlan"] = viz_plan
# ── Step 4: Commit revision ───────────────────────────────────────────
component_ids = [c["componentId"] for c in viz_plan.get("components", [])]
execution["componentsCreated"] = component_ids
# Commit a revision bump with the new components
try:
page = await canvas_service.get_page(page_id, tenant_id)
if page:
existing_comps = page.get("components", [])
new_comps = existing_comps + viz_plan.get("components", [])
revision = await canvas_service.commit_revision(
page_id=page_id,
tenant_id=tenant_id,
actor_id=actor_id,
commit_kind="prompt",
commit_summary=f"Oracle: {prompt[:80]}",
components=new_comps,
execution_id=execution_id,
idempotency_key=client_request_id,
)
execution["headRevision"] = revision["revisionNumber"]
except Exception as exc:
logger.warning("ORCH revision_commit failed (non-fatal): %s", exc)
warnings.append("Revision commit deferred — will retry on next sync.")
execution["status"] = "completed"
execution["summary"] = self._generate_summary(prompt, viz_plan)
execution["completedAt"] = _now()
execution["warnings"] = warnings
await self._persist_execution(execution)
return execution
async def _build_visualization_plan(
self,
*,
retrieval_plan: dict[str, Any],
prompt: str,
execution_id: str,
actor_id: str,
tenant_id: str,
branch_id: str,
placement_mode: str,
ctx: PolicyContext,
persona_plan: dict[str, Any],
) -> dict[str, Any]:
"""Converts a retrieval plan into a list of CanvasComponent descriptors."""
components = [
self._persona_text_canvas(
execution_id=execution_id,
actor_id=actor_id,
branch_id=branch_id,
prompt=prompt,
persona_plan=persona_plan,
)
]
base_order = 900 # Append after existing components
component_plans = retrieval_plan.get("components", [])
for i, plan in enumerate(component_plans):
ctype = plan["suggestedType"]
dataset = plan["dataset"]
component_id = str(uuid.uuid4())
query_result = await data_access_gateway.execute_component_plan(plan, ctx, prompt)
component_warnings = query_result.warnings
mapped_type = self._map_type(ctype)
data_rows = query_result.rows
comp: dict[str, Any] = {
"componentId": component_id,
"type": mapped_type,
"title": str(plan.get("titleHint") or self._generate_title(prompt, ctype)),
"description": f"Generated from: \"{prompt[:80]}\"",
"dataSourceDescriptor": {
"descriptorId": str(uuid.uuid4()),
"sourceType": "postgres",
"connectorId": "velocity-core-postgres",
"dataset": dataset,
"authContextRef": f"authctx_{actor_id}_scope",
"queryTemplate": plan.get("queryTemplate", f"SELECT * FROM {dataset} WHERE tenant_id = :tenant_id"),
"queryParameters": plan.get("queryParameters", {"tenant_id": tenant_id}),
"rowLimit": plan.get("rowLimit", 50),
"privacyTier": plan.get("privacyTier", "standard"),
"cachePolicy": {"mode": "ttl", "ttlSeconds": 120},
},
"visualizationParameters": self._default_viz_params(ctype, data_rows),
"dataBindings": self._default_bindings(ctype),
"version": 1,
"lifecycleState": "active",
"provenance": {
"originType": "prompt_generated",
"promptExecutionId": execution_id,
"sourceBranchId": branch_id,
"createdBy": actor_id,
"createdAt": _now(),
},
"renderingHints": self._rendering_hints(ctype),
"layout": {
"orderIndex": base_order + (i + 1) * 100,
"sectionId": "sec_prompt_generated",
"widthMode": "full" if ctype in ("pipeline_board", "table", "geo_map") else "half",
"minHeightPx": 300,
"stickyHeader": False,
},
"accessControls": {
"visibilityScope": "private",
"allowedRoles": ["senior_broker", "sales_director", "marketing_operator", "data_steward", "compliance_reviewer", "platform_admin"],
"redactionPolicy": "none",
},
"styleSignature": {
"theme": "velocity_glass",
"paletteToken": "ocean_signal",
"motionProfile": "calm_reveal",
"density": "comfortable",
"radiusScale": "lg",
"typographyScale": "balanced",
},
"validationState": {
"schema": "pass",
"policy": "pass",
"a11y": "pass",
"performance": "pass",
"status": "validated",
},
"auditLog": [f"aud_{execution_id}_create"],
"dataRows": data_rows,
}
if component_warnings and not data_rows:
comp = self._error_component(
component_id=component_id,
execution_id=execution_id,
actor_id=actor_id,
branch_id=branch_id,
dataset=dataset,
warnings=component_warnings,
order_index=base_order + (i + 1) * 100,
)
components.append(comp)
return {"components": components}
@staticmethod
def _persona_text_canvas(
*,
execution_id: str,
actor_id: str,
branch_id: str,
prompt: str,
persona_plan: dict[str, Any],
) -> dict[str, Any]:
recommended = ", ".join(persona_plan.get("recommendedTemplates", [])) or "no direct template matches"
content = (
f"Oracle received: {prompt}\n\n"
f"Reusable templates: {recommended}\n\n"
"Execution policy: query live CRM data first, reuse matching templates, "
"synthesize missing UI blocks, then dispatch the required ComfyUI-backed workflow."
)
return {
"componentId": str(uuid.uuid4()),
"type": "textCanvas",
"title": "Oracle Planning Notes",
"description": "Persona-driven guidance generated before data-bound components.",
"dataSourceDescriptor": {
"descriptorId": str(uuid.uuid4()),
"sourceType": "inline",
"connectorId": "oracle-persona",
"dataset": "oracle_persona_plan",
"authContextRef": f"authctx_{actor_id}_scope",
"queryTemplate": "",
"queryParameters": {},
"rowLimit": 1,
"privacyTier": "standard",
},
"visualizationParameters": {
"content": content,
"widthMode": "full",
"adjustableHeight": True,
},
"dataBindings": {"dimensions": [], "measures": [], "series": [], "filters": []},
"version": 1,
"lifecycleState": "active",
"provenance": {
"originType": "prompt_generated",
"promptExecutionId": execution_id,
"sourceBranchId": branch_id,
"createdBy": actor_id,
"createdAt": _now(),
},
"renderingHints": {"estimatedHeightPx": 180, "skeletonVariant": "text", "virtualizationPriority": 4},
"layout": {
"orderIndex": 910,
"sectionId": "sec_prompt_generated",
"widthMode": "full",
"minHeightPx": 180,
"stickyHeader": False,
},
"accessControls": {
"visibilityScope": "private",
"allowedRoles": ["senior_broker", "sales_director", "marketing_operator", "data_steward", "compliance_reviewer", "platform_admin"],
"redactionPolicy": "none",
},
"styleSignature": {
"theme": "velocity_glass",
"paletteToken": "ocean_signal",
"motionProfile": "calm_reveal",
"density": "comfortable",
"radiusScale": "lg",
"typographyScale": "balanced",
},
"validationState": {
"schema": "pass",
"policy": "pass",
"a11y": "pass",
"performance": "pass",
"status": "validated",
},
"auditLog": [f"aud_{execution_id}_persona"],
"dataRows": [],
}
@staticmethod
def _map_type(plan_type: str) -> str:
mapping = {
"pipeline_board": "pipelineBoard",
"bar_chart": "barChart",
"geo_map": "geoMap",
"table": "table",
"line_chart": "lineChart",
"kpi_tile": "kpiTile",
"activity_stream": "activityStream",
}
return mapping.get(plan_type, "barChart")
@staticmethod
def _generate_title(prompt: str, comp_type: str) -> str:
labels = {
"pipeline_board": "Pipeline View",
"bar_chart": "Comparative Analysis",
"geo_map": "Geographic Distribution",
"table": "Performance Table",
"line_chart": "Trend Analysis",
"kpi_tile": "Key Metric",
"activity_stream": "Activity Stream",
}
return labels.get(comp_type, "Oracle Canvas Component")
@staticmethod
def _default_viz_params(comp_type: str, rows: list[dict[str, Any]]) -> dict[str, Any]:
defaults: dict[str, dict[str, Any]] = {
"bar_chart": {"xAxis": "category", "yAxis": "value", "sort": "desc", "showLabels": True, "legend": False},
"line_chart": {"showPoints": True, "smooth": True},
"kpi_tile": {
"label": rows[0].get("metric_label", "Result") if rows else "Result",
"trend": str(rows[0].get("trend_value", "")) if rows else "",
"comparisonLabel": rows[0].get("comparison_label", "") if rows else "",
},
"geo_map": {"mapStyle": "dubai_district_heat", "intensityField": "lead_count", "interactive": True, "tooltipFields": ["district", "lead_count", "avg_qd_score"]},
"table": {"rankBy": "revenue_generated", "showTopBadge": True, "columns": ["name", "deals_closed", "revenue_generated"]},
"pipeline_board": {"showValue": True, "colorByStage": True},
"activity_stream": {"showUrgencyIndicator": True},
}
return defaults.get(comp_type, {})
@staticmethod
def _default_bindings(comp_type: str) -> dict[str, Any]:
return {"dimensions": [], "measures": [], "series": [], "filters": []}
@staticmethod
def _rendering_hints(comp_type: str) -> dict[str, Any]:
priority_map = {
"pipeline_board": ("pipeline", 9), "bar_chart": ("chart", 8),
"geo_map": ("map", 9), "table": ("table", 7),
"line_chart": ("chart", 8), "kpi_tile": ("kpi", 6),
"activity_stream": ("table", 8),
}
skeleton, priority = priority_map.get(comp_type, ("chart", 7))
height_map = {
"pipeline_board": 400, "bar_chart": 320, "geo_map": 420,
"table": 300, "line_chart": 320, "kpi_tile": 140, "activity_stream": 360,
}
return {
"estimatedHeightPx": height_map.get(comp_type, 300),
"skeletonVariant": skeleton,
"virtualizationPriority": priority,
}
@staticmethod
def _generate_summary(prompt: str, viz_plan: dict[str, Any]) -> str:
count = len(viz_plan.get("components", []))
short_prompt = prompt[:60] + ("" if len(prompt) > 60 else "")
return f'Generated {count} component{"s" if count != 1 else ""} for: "{short_prompt}"'
@staticmethod
def _error_component(
*,
component_id: str,
execution_id: str,
actor_id: str,
branch_id: str,
dataset: str,
warnings: list[str],
order_index: int,
) -> dict[str, Any]:
return {
"componentId": component_id,
"type": "errorNotice",
"title": f"{dataset} unavailable",
"description": "Oracle could not render live data for this component.",
"dataSourceDescriptor": {
"descriptorId": str(uuid.uuid4()),
"sourceType": "postgres",
"connectorId": "velocity-core-postgres",
"dataset": dataset,
"authContextRef": f"authctx_{actor_id}_scope",
"queryTemplate": "",
"queryParameters": {},
"rowLimit": 0,
"privacyTier": "standard",
},
"visualizationParameters": {
"errorCode": "oracle_live_query_failed",
"message": " | ".join(warnings[:2]),
"severity": "warning",
"retryable": True,
},
"dataBindings": {"dimensions": [], "measures": [], "series": [], "filters": []},
"version": 1,
"lifecycleState": "active",
"provenance": {
"originType": "prompt_generated",
"promptExecutionId": execution_id,
"sourceBranchId": branch_id,
"createdBy": actor_id,
"createdAt": _now(),
},
"renderingHints": {"estimatedHeightPx": 140, "skeletonVariant": "generic", "virtualizationPriority": 5},
"layout": {
"orderIndex": order_index,
"sectionId": "sec_prompt_generated",
"widthMode": "full",
"minHeightPx": 140,
"stickyHeader": False,
},
"accessControls": {
"visibilityScope": "private",
"allowedRoles": ["senior_broker", "sales_director", "marketing_operator", "data_steward", "compliance_reviewer", "platform_admin"],
"redactionPolicy": "none",
},
"styleSignature": {
"theme": "velocity_glass",
"paletteToken": "ocean_signal",
"motionProfile": "calm_reveal",
"density": "comfortable",
"radiusScale": "lg",
"typographyScale": "balanced",
},
"validationState": {
"schema": "pass",
"policy": "pass",
"a11y": "pass",
"performance": "pass",
"status": "validated",
},
"auditLog": [f"aud_{execution_id}_error"],
"dataRows": [],
}
async def _call_nemoclaw(
self,
prompt: str,
context: list[dict[str, str]],
ctx: PolicyContext,
) -> dict[str, Any]:
"""
Uses the shared runtime LLM service to propose a retrieval plan.
Raises on malformed output so the orchestrator can fall back safely.
"""
row_limit = 50 if ctx.actor_role in ("senior_broker", "junior_broker") else 200
system_prompt = (
"You are the Oracle planner for Project Velocity. "
"Return JSON only. "
"Choose up to 4 analytical components for the prompt. "
"Allowed component types: pipeline_board, bar_chart, geo_map, table, line_chart, kpi_tile, activity_stream. "
"Allowed datasets: deals, lead_daily_snapshot, lead_geo_interest_rollup, broker_performance, inventory_absorption, "
"oracle_aggregated_metric, lead_activity_log, crm_contacts_overview, crm_opportunity_pipeline, "
"crm_property_interest_rollup, crm_interaction_timeline. "
"Return an object with keys semanticModelVersion, intentClass, components. "
"Each component must include suggestedType, dataset, and titleHint. "
"Do not emit SQL. Do not invent datasets outside the allowlist."
)
response = await runtime_llm_service.chat(
provider_id=None,
model=None,
system_prompt=system_prompt,
messages=[
*context,
{
"role": "user",
"content": json.dumps(
{
"prompt": prompt,
"tenantId": ctx.tenant_id,
"actorRole": ctx.actor_role,
"rowLimit": row_limit,
}
),
},
],
temperature=0.1,
response_format="json",
metadata={"planner": "oracle_canvas"},
)
payload = response.get("message", {}).get("parsedJson") or {}
components_payload = payload.get("components")
if not isinstance(components_payload, list) or not components_payload:
raise ValueError("Runtime LLM planner returned no components.")
normalized_components: list[dict[str, Any]] = []
for raw_component in components_payload[:4]:
if not isinstance(raw_component, dict):
continue
suggested_type = str(raw_component.get("suggestedType", "")).strip()
dataset = str(raw_component.get("dataset", "")).strip()
if suggested_type not in _DATASET_MAP or dataset not in _RUNTIME_ALLOWED_DATASETS:
continue
normalized_components.append(
{
"suggestedType": suggested_type,
"dataset": dataset,
"privacyTier": "standard",
"rowLimit": row_limit,
"joins": [],
"queryTemplate": f"SELECT * FROM {dataset} WHERE tenant_id = :tenant_id LIMIT :limit",
"queryParameters": {"tenant_id": ctx.tenant_id, "limit": row_limit},
"titleHint": str(raw_component.get("titleHint", "")).strip() or self._generate_title(prompt, suggested_type),
}
)
if not normalized_components:
raise ValueError("Runtime LLM planner returned no valid whitelisted components.")
return {
"planId": str(uuid.uuid4()),
"components": normalized_components,
"semanticModelVersion": str(payload.get("semanticModelVersion") or "oracle_runtime_llm_v2026_04_19_01"),
"intentClass": str(payload.get("intentClass") or "analytical"),
"planner": "runtime_llm",
}
async def get_execution(self, execution_id: str) -> dict[str, Any] | None:
return _DEMO_EXECUTIONS.get(execution_id)
async def _persist_execution(self, execution: dict[str, Any]) -> None:
_DEMO_EXECUTIONS[execution["executionId"]] = execution
if not _db_ready():
return
assert asyncpg is not None
conn = await asyncpg.connect(_DB_URL)
try:
await conn.execute(
"""
INSERT INTO oracle_prompt_executions (
execution_id, tenant_id, page_id, branch_id, actor_id, prompt, intent_class,
status, model_runtime, semantic_model_version, retrieval_plan, visualization_plan,
warnings, summary, components_created, client_request_id, created_at, completed_at
)
VALUES (
$1::uuid, $2, $3::uuid, $4, $5, $6, $7,
$8, $9, $10, $11::jsonb, $12::jsonb,
$13::text[], $14, $15::text[], $16, $17::timestamptz, $18::timestamptz
)
ON CONFLICT (execution_id)
DO UPDATE SET
status = EXCLUDED.status,
retrieval_plan = EXCLUDED.retrieval_plan,
visualization_plan = EXCLUDED.visualization_plan,
warnings = EXCLUDED.warnings,
summary = EXCLUDED.summary,
components_created = EXCLUDED.components_created,
completed_at = EXCLUDED.completed_at
""",
execution["executionId"],
execution["tenantId"],
execution["pageId"],
execution["branchId"],
execution["actorId"],
execution["prompt"],
execution["intentClass"],
execution["status"],
execution["modelRuntime"],
execution["semanticModelVersion"],
json.dumps(execution.get("retrievalPlan") or {}),
json.dumps(execution.get("visualizationPlan") or {}),
execution.get("warnings", []),
execution.get("summary"),
execution.get("componentsCreated", []),
execution.get("clientRequestId"),
execution["createdAt"],
execution.get("completedAt"),
)
finally:
await conn.close()
# ── Singleton ─────────────────────────────────────────────────────────────────
prompt_orchestrator = PromptOrchestrator()

View File

@@ -0,0 +1,458 @@
"""
oracle/router_v1.py
FastAPI router for all Oracle v1 endpoints.
Mounted at /api/oracle/v1 in main.py.
Endpoints (from spec §13.2):
GET /me
GET /canvas-pages/{pageId}
POST /canvas-pages/{pageId}/prompts
POST /canvas-pages/{pageId}/forks
POST /canvas-pages/{pageId}/rollback
GET /canvas-pages/{pageId}/revisions
GET /component-templates
POST /component-templates/synthesize (stub)
GET /merge-requests
POST /merge-requests
POST /merge-requests/{mrId}/review
WS /ws/oracle/canvas/{pageId}
"""
from __future__ import annotations
import json
import logging
import os
import uuid
from datetime import datetime, timezone
from typing import Any, Set
from fastapi import APIRouter, Depends, HTTPException, Request, WebSocket, WebSocketDisconnect, status
from pydantic import BaseModel, Field
from backend.auth.dependencies import UserPrincipal, get_current_user
from .canvas_service import canvas_service
from .collaboration_service import collaboration_service
from .action_service import oracle_action_service
from .persona_service import persona_service
from .prompt_orchestrator import prompt_orchestrator
from .policy_service import PolicyService, PolicyContext
from .codebook_service import codebook_service
logger = logging.getLogger(__name__)
router = APIRouter()
policy_svc = PolicyService()
_DEFAULT_TENANT_ID = os.getenv("ORACLE_DEFAULT_TENANT_ID", "tenant_velocity")
# ── Helpers ───────────────────────────────────────────────────────────────────
def _ok(data: Any, meta: dict | None = None) -> dict:
return {"status": "ok", "data": data, "meta": meta or {}}
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def _normalize_oracle_role(role: str) -> str:
mapping = {
"JUNIOR_BROKER": "junior_broker",
"SENIOR_BROKER": "senior_broker",
"SALES_DIRECTOR": "sales_director",
"ADMIN": "platform_admin",
}
return mapping.get(role.strip().upper(), "sales_director")
def _build_user_profile(
*,
user_id: str,
email: str,
display_name: str,
role: str,
avatar_url: str | None,
default_page_id: str,
) -> dict[str, Any]:
return {
"userId": user_id,
"tenantId": _DEFAULT_TENANT_ID,
"email": email,
"displayName": display_name,
"role": _normalize_oracle_role(role),
"avatarUrl": avatar_url,
"timezone": os.getenv("ORACLE_DEFAULT_TIMEZONE", "Asia/Dubai"),
"locale": os.getenv("ORACLE_DEFAULT_LOCALE", "en-AE"),
"defaultPageId": default_page_id,
"canvasPreferences": {
"defaultDensity": "comfortable",
"defaultPlacementMode": "append_after_last_visible_component",
"showLineageBadges": True,
},
"policyProfileId": os.getenv("ORACLE_POLICY_PROFILE_ID", "policy_sales_director_standard_v4"),
"createdAt": os.getenv("ORACLE_PROFILE_CREATED_AT", _now()),
"updatedAt": _now(),
}
async def _get_current_user_profile(request: Request, user: UserPrincipal) -> dict[str, Any]:
seed_page = await canvas_service.ensure_default_page(
tenant_id=_DEFAULT_TENANT_ID,
owner_id=user.user_id,
title=os.getenv("ORACLE_DEFAULT_PAGE_TITLE", "Oracle Main Canvas"),
)
pool = getattr(request.app.state, "db_pool", None)
if pool is None:
raise HTTPException(status_code=503, detail="Database unavailable.")
async with pool.acquire() as conn:
row = await conn.fetchrow(
"""
SELECT
COALESCE(full_name, split_part(email, '@', 1), id::text) AS display_name,
COALESCE(email, id::text || '@velocity.local') AS email,
avatar_url
FROM users_and_roles
WHERE id = $1::uuid
""",
user.user_id,
)
return _build_user_profile(
user_id=user.user_id,
email=row["email"] if row else f"{user.user_id}@velocity.local",
display_name=row["display_name"] if row else user.user_id,
role=user.role,
avatar_url=row["avatar_url"] if row else None,
default_page_id=seed_page["pageId"],
)
async def _ctx_from_request(request: Request, user: UserPrincipal) -> PolicyContext:
me = await _get_current_user_profile(request, user)
return PolicyContext(
tenant_id=me["tenantId"],
actor_id=me["userId"],
actor_role=me["role"],
)
# ── Pydantic Models ───────────────────────────────────────────────────────────
class PromptSubmitRequest(BaseModel):
clientRequestId: str = Field(..., description="Client-generated idempotency key")
branchId: str
prompt: str = Field(..., min_length=1, max_length=4096)
conversationContext: list[dict[str, str]] = Field(default_factory=list)
placementMode: str = Field("append_after_last_visible_component")
targetLeadId: str | None = None
plannedWriteback: dict[str, Any] = Field(default_factory=dict)
class ForkCreateRequest(BaseModel):
recipientUserId: str
sourceRevision: int
visibility: str = Field("private", pattern="^(private|team)$")
message: str = ""
class RollbackRequest(BaseModel):
targetRevision: int = Field(..., ge=1)
clientRequestId: str
class MergeRequestCreateRequest(BaseModel):
sourcePageId: str
sourceBranchId: str
targetPageId: str
targetBranchId: str
title: str = Field(..., min_length=1, max_length=256)
description: str = ""
class MergeReviewRequest(BaseModel):
decision: str = Field(..., pattern="^(approve|reject|changes_requested)$")
comment: str = ""
resolutions: list[dict[str, Any]] = Field(default_factory=list)
class TemplateSynthesizeRequest(BaseModel):
prompt: str
dataShape: list[str]
styleSignatureRef: str | None = None
class PersonaRenderRequest(BaseModel):
promptName: str = Field(..., pattern="^(qd_calculator|lead_tagger|cctv_profiler)$")
variables: dict[str, Any] = Field(default_factory=dict)
# ── Endpoints ─────────────────────────────────────────────────────────────────
@router.get("/me", summary="Get current user profile")
async def get_me(request: Request, user: UserPrincipal = Depends(get_current_user)) -> dict:
return _ok(await _get_current_user_profile(request, user))
@router.get("/canvas-pages/{page_id}", summary="Get canvas page by ID")
async def get_canvas_page(page_id: str, request: Request, user: UserPrincipal = Depends(get_current_user)) -> dict:
ctx = await _ctx_from_request(request, user)
page = await canvas_service.get_page(page_id, ctx.tenant_id)
if not page:
raise HTTPException(status_code=404, detail=f"Canvas page '{page_id}' not found.")
return _ok(page)
@router.post("/canvas-pages/{page_id}/prompts", summary="Submit a prompt to generate canvas components")
async def submit_prompt(
page_id: str,
payload: PromptSubmitRequest,
request: Request,
user: UserPrincipal = Depends(get_current_user),
) -> dict:
ctx = await _ctx_from_request(request, user)
execution = await prompt_orchestrator.execute(
tenant_id=ctx.tenant_id,
page_id=page_id,
branch_id=payload.branchId,
actor_id=ctx.actor_id,
actor_role=ctx.actor_role,
prompt=payload.prompt,
conversation_context=payload.conversationContext,
client_request_id=payload.clientRequestId,
placement_mode=payload.placementMode,
)
if execution["status"] == "failed":
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail={"errors": execution.get("warnings", [])},
)
page = await canvas_service.get_page(page_id, ctx.tenant_id)
action = await oracle_action_service.create_from_execution(
execution=execution,
target_entity_type="lead" if payload.targetLeadId else "canvas_page",
target_entity_id=payload.targetLeadId or page_id,
action_type="oracle_prompt_writeback_plan" if payload.targetLeadId else "oracle_canvas_generation",
writeback_payload=payload.plannedWriteback,
)
return _ok({
"executionId": execution["executionId"],
"actionId": action["actionId"],
"status": execution["status"],
"pageId": page_id,
"branchId": payload.branchId,
"headRevision": execution.get("headRevision", page.get("headRevision", 0) if page else 0),
"componentsCreated": execution.get("componentsCreated", []),
"summary": execution.get("summary", ""),
"warnings": execution.get("warnings", []),
"components": page.get("components", []) if page else [],
})
@router.post("/canvas-pages/{page_id}/forks", summary="Create a fork (share) from a canvas page")
async def create_fork(
page_id: str,
payload: ForkCreateRequest,
request: Request,
user: UserPrincipal = Depends(get_current_user),
) -> dict:
ctx = await _ctx_from_request(request, user)
page = await canvas_service.get_page(page_id, ctx.tenant_id)
if not page:
raise HTTPException(status_code=404, detail="Source page not found.")
fork = await collaboration_service.create_fork(
source_page=page,
recipient_user_id=payload.recipientUserId,
created_by=ctx.actor_id,
visibility=payload.visibility,
message=payload.message,
)
return _ok(fork)
@router.post("/canvas-pages/{page_id}/rollback", summary="Rollback canvas to a prior revision")
async def rollback_canvas(
page_id: str,
payload: RollbackRequest,
request: Request,
user: UserPrincipal = Depends(get_current_user),
) -> dict:
ctx = await _ctx_from_request(request, user)
result = await canvas_service.rollback(
page_id=page_id,
tenant_id=ctx.tenant_id,
actor_id=ctx.actor_id,
target_revision=payload.targetRevision,
idempotency_key=payload.clientRequestId,
)
page = await canvas_service.get_page(page_id, ctx.tenant_id)
return _ok({
"pageId": page_id,
"headRevision": result.get("revisionNumber", payload.targetRevision),
"components": page.get("components", []) if page else [],
})
@router.get("/canvas-pages/{page_id}/revisions", summary="List revision history for a canvas page")
async def list_revisions(page_id: str, request: Request, user: UserPrincipal = Depends(get_current_user)) -> dict:
ctx = await _ctx_from_request(request, user)
revisions = await canvas_service.list_revisions(page_id, ctx.tenant_id)
return _ok(revisions, meta={"count": len(revisions)})
@router.get("/component-templates", summary="List component templates")
async def list_templates(
category: str | None = None,
status: str | None = None,
search: str | None = None,
limit: int = 50,
offset: int = 0,
) -> dict:
result = codebook_service.list_templates(
category=category,
status=status,
search=search,
limit=limit,
offset=offset,
)
return _ok(result["templates"], meta={"count": result["total"], "limit": limit, "offset": offset})
@router.post("/component-templates/synthesize", summary="Synthesize a new component template from a prompt")
async def synthesize_template(
payload: TemplateSynthesizeRequest,
request: Request,
user: UserPrincipal = Depends(get_current_user),
) -> dict:
me = await _get_current_user_profile(request, user)
template = codebook_service.synthesize_template(
prompt=payload.prompt,
data_shapes=payload.dataShape,
)
template["tenantId"] = me["tenantId"]
template.setdefault("createdAt", _now())
template.setdefault("updatedAt", _now())
return _ok(template)
@router.get("/persona/health", summary="Health check for Oracle persona prompt loading")
async def persona_health() -> dict:
return _ok(await persona_service.health())
@router.post("/persona/render", summary="Render a subordinate Oracle persona prompt")
async def persona_render(payload: PersonaRenderRequest) -> dict:
try:
rendered = await persona_service.render_prompt(
prompt_name=payload.promptName,
variables=payload.variables,
)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
return _ok(rendered)
@router.get("/merge-requests", summary="List merge requests for a target page")
async def list_merge_requests(targetPageId: str | None = None, status: str | None = None) -> dict:
if not targetPageId:
raise HTTPException(status_code=400, detail="targetPageId query param required")
mrs = await collaboration_service.list_merge_requests(targetPageId, status)
return _ok(mrs, meta={"count": len(mrs)})
@router.post("/merge-requests", summary="Open a merge request")
async def create_merge_request(
payload: MergeRequestCreateRequest,
request: Request,
user: UserPrincipal = Depends(get_current_user),
) -> dict:
ctx = await _ctx_from_request(request, user)
source_page = await canvas_service.get_page(payload.sourcePageId, ctx.tenant_id)
target_page = await canvas_service.get_page(payload.targetPageId, ctx.tenant_id)
if not source_page or not target_page:
raise HTTPException(status_code=404, detail="Source or target page not found.")
mr = await collaboration_service.open_merge_request(
tenant_id=ctx.tenant_id,
source_page_id=payload.sourcePageId,
source_branch_id=payload.sourceBranchId,
source_head_revision=source_page.get("headRevision", 0),
target_page_id=payload.targetPageId,
target_branch_id=payload.targetBranchId,
target_base_revision=target_page.get("headRevision", 0),
title=payload.title,
description=payload.description,
created_by=ctx.actor_id,
source_components=source_page.get("components", []),
target_components=target_page.get("components", []),
base_components=[], # Simplified: empty base for demo
)
return _ok(mr)
@router.post("/merge-requests/{mr_id}/review", summary="Submit a merge request review")
async def review_merge_request(
mr_id: str,
payload: MergeReviewRequest,
request: Request,
user: UserPrincipal = Depends(get_current_user),
) -> dict:
ctx = await _ctx_from_request(request, user)
mr = await collaboration_service.review_merge_request(
mr_id=mr_id,
decision=payload.decision,
reviewer_id=ctx.actor_id,
comment=payload.comment,
resolutions=payload.resolutions,
)
return _ok(mr)
# ── WebSocket ─────────────────────────────────────────────────────────────────
class OracleConnectionManager:
def __init__(self) -> None:
self.active: dict[str, Set[WebSocket]] = {}
async def connect(self, ws: WebSocket, page_id: str) -> None:
await ws.accept()
self.active.setdefault(page_id, set()).add(ws)
def disconnect(self, ws: WebSocket, page_id: str) -> None:
page_connections = self.active.get(page_id, set())
page_connections.discard(ws)
async def broadcast_page(self, page_id: str, payload: dict) -> None:
dead: set[WebSocket] = set()
for ws in self.active.get(page_id, set()):
try:
await ws.send_text(json.dumps(payload))
except Exception:
dead.add(ws)
if dead:
self.active.get(page_id, set()).difference_update(dead)
oracle_manager = OracleConnectionManager()
@router.websocket("/ws/oracle/canvas/{page_id}")
async def oracle_canvas_ws(ws: WebSocket, page_id: str) -> None:
"""
WebSocket endpoint for real-time Oracle canvas collaboration.
Event types: oracle.page.revision.committed, oracle.prompt.received, oracle.presence.updated
"""
await oracle_manager.connect(ws, page_id)
try:
while True:
data = await ws.receive_text()
try:
msg = json.loads(data)
# Reflect heartbeat
if msg.get("type") == "heartbeat":
await ws.send_text(json.dumps({"type": "heartbeat.ack", "timestamp": _now()}))
except json.JSONDecodeError:
pass
except WebSocketDisconnect:
oracle_manager.disconnect(ws, page_id)
# ── Pre-made templates seed ───────────────────────────────────────────────────

View File

@@ -0,0 +1,103 @@
from __future__ import annotations
import json
from pathlib import Path
from backend.oracle.codebook_service import (
_repo_root,
_safe_load_json,
_normalize_examples,
)
def main() -> None:
root = _repo_root()
primary_path = root / ".Agent Context" / "Sprint 1" / "Sayan Multi-Surface and Oracle Delivery Pack" / "Sample JSON Schema" / "GPT 5.4" / "oracle_canvas_json_expansion_pack" / "db" / "oracle_template_seed_db_expanded_v1.pretty.json"
secondary_path = root / ".Agent Context" / "Sprint 1" / "Sayan Multi-Surface and Oracle Delivery Pack" / "Sample JSON Schema" / "Claude Sonnet 4.6" / "oracle_template_expansion" / "oracle_template_seed_db_expanded.json"
fallback_path = root / "backend" / "oracle" / "oracle_template_seed_db.json"
output_path = root / "backend" / "oracle" / "oracle_runtime_codebook_merged.json"
corpora = []
for path, label in (
(primary_path, "gpt_5_4"),
(secondary_path, "claude_sonnet_4_6"),
(fallback_path, "runtime_seed_fallback"),
):
if path.exists():
corpora.extend(_normalize_examples(_safe_load_json(path), label))
deduped = {}
for example in corpora:
key = (example.subchapter_id, example.template_name.lower(), example.title.lower())
existing = deduped.get(key)
if existing is None:
deduped[key] = example
continue
if example.source_pack == "gpt_5_4" and existing.source_pack != "gpt_5_4":
deduped[key] = example
elif example.is_canonical and not existing.is_canonical:
deduped[key] = example
examples = list(deduped.values())
chapters: dict[str, dict] = {}
for example in examples:
chapter = chapters.setdefault(
example.chapter_id,
{
"chapter_id": example.chapter_id,
"name": example.chapter_name,
"subchapters": {},
},
)
chapter["subchapters"].setdefault(
example.subchapter_id,
{
"subchapter_id": example.subchapter_id,
"name": example.subchapter_name,
},
)
payload = {
"_meta": {
"generated_by": "backend/scripts/build_oracle_runtime_codebook.py",
"source_priority": ["gpt_5_4", "claude_sonnet_4_6", "runtime_seed_fallback"],
"example_count": len(examples),
},
"chapters": [
{
"chapter_id": chapter["chapter_id"],
"name": chapter["name"],
"subchapters": list(chapter["subchapters"].values()),
}
for chapter in sorted(chapters.values(), key=lambda item: item["chapter_id"])
],
"seed_examples": [
{
"example_id": example.example_id,
"chapter_id": example.chapter_id,
"subchapter_id": example.subchapter_id,
"title": example.title,
"template_name": example.template_name,
"component_type": example.component_type,
"accepted_shapes": list(example.accepted_shapes),
"example_json": example.example_json,
"quality_notes": example.quality_notes,
"is_canonical": example.is_canonical,
"source_pack": example.source_pack,
"surface_targets": list(example.surface_targets),
"policy_tags": list(example.policy_tags),
"backend_contract_hints": example.backend_contract_hints,
}
for example in sorted(
examples,
key=lambda item: (item.chapter_id, item.subchapter_id, item.template_name.lower(), item.title.lower()),
)
],
}
output_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Wrote merged Oracle runtime codebook to {output_path}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,461 @@
from __future__ import annotations
import asyncio
import json
import logging
import os
import uuid
from dataclasses import dataclass
from datetime import UTC, datetime
from typing import Any
import httpx
logger = logging.getLogger("velocity.runtime_llm")
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://127.0.0.1:11434").rstrip("/")
OLLAMA_CHAT_URL = os.getenv("OLLAMA_CHAT_URL", f"{OLLAMA_BASE_URL}/v1/chat/completions")
OLLAMA_TAGS_URL = os.getenv("OLLAMA_TAGS_URL", f"{OLLAMA_BASE_URL}/api/tags")
OLLAMA_DEFAULT_MODEL = os.getenv("OLLAMA_MODEL", "qwen3.5:27b")
NEMOCLAW_BASE_URL = os.getenv("NEMOCLAW_BASE_URL", "").rstrip("/")
NEMOCLAW_CHAT_URL = (os.getenv("NEMOCLAW_CHAT_URL") or f"{NEMOCLAW_BASE_URL}/v1/chat/completions").rstrip("/") if NEMOCLAW_BASE_URL else ""
NEMOCLAW_DEFAULT_MODEL = os.getenv("NEMOCLAW_MODEL", "nvidia/nemotron-3-super-120b-a12b")
NEMOCLAW_API_TOKEN = os.getenv("NEMOCLAW_API_TOKEN", "")
RUNTIME_LLM_TIMEOUT_S = float(os.getenv("RUNTIME_LLM_TIMEOUT_S", "90.0"))
RUNTIME_LLM_CONCURRENCY = int(os.getenv("RUNTIME_LLM_BATCH_CONCURRENCY", "2"))
def _utc_now() -> datetime:
return datetime.now(UTC)
def _utc_iso() -> str:
return _utc_now().isoformat()
@dataclass
class RuntimeProvider:
provider_id: str
base_url: str
chat_url: str
default_model: str
auth_token: str | None = None
supports_batch: bool = True
@property
def headers(self) -> dict[str, str]:
headers = {"Content-Type": "application/json"}
if self.auth_token:
headers["Authorization"] = f"Bearer {self.auth_token}"
return headers
class RuntimeLLMService:
def __init__(self) -> None:
self._jobs: dict[str, dict[str, Any]] = {}
def _provider_catalog(self) -> list[RuntimeProvider]:
providers: list[RuntimeProvider] = []
if OLLAMA_CHAT_URL:
providers.append(
RuntimeProvider(
provider_id="ollama",
base_url=OLLAMA_BASE_URL,
chat_url=OLLAMA_CHAT_URL,
default_model=OLLAMA_DEFAULT_MODEL,
)
)
if NEMOCLAW_CHAT_URL:
providers.append(
RuntimeProvider(
provider_id="nemoclaw",
base_url=NEMOCLAW_BASE_URL,
chat_url=NEMOCLAW_CHAT_URL,
default_model=NEMOCLAW_DEFAULT_MODEL,
auth_token=NEMOCLAW_API_TOKEN or None,
)
)
return providers
def get_provider(self, provider_id: str | None) -> RuntimeProvider:
providers = {provider.provider_id: provider for provider in self._provider_catalog()}
if provider_id:
provider = providers.get(provider_id)
if provider is None:
raise ValueError(f"Unknown provider '{provider_id}'.")
return provider
if "nemoclaw" in providers:
return providers["nemoclaw"]
if "ollama" in providers:
return providers["ollama"]
raise ValueError("No runtime LLM providers are configured.")
async def list_providers(self) -> list[dict[str, Any]]:
providers: list[dict[str, Any]] = []
for provider in self._provider_catalog():
models: list[str] = [provider.default_model]
status = "offline"
error: str | None = None
try:
if provider.provider_id == "ollama":
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(OLLAMA_TAGS_URL)
response.raise_for_status()
payload = response.json()
models = [str(item.get("name", "")).strip() for item in payload.get("models", []) if item.get("name")]
if provider.default_model not in models:
models.insert(0, provider.default_model)
status = "online"
else:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.post(
provider.chat_url,
json={
"model": provider.default_model,
"messages": [{"role": "user", "content": "ping"}],
"max_tokens": 4,
},
headers=provider.headers,
)
response.raise_for_status()
status = "online"
except Exception as exc: # pragma: no cover - network/runtime dependent
error = str(exc)
providers.append(
{
"id": provider.provider_id,
"status": status,
"baseUrl": provider.base_url,
"defaultModel": provider.default_model,
"models": models,
"supportsBatch": provider.supports_batch,
"error": error,
}
)
return providers
async def chat(
self,
*,
provider_id: str | None,
model: str | None,
system_prompt: str | None,
messages: list[dict[str, str]],
temperature: float = 0.2,
response_format: str | None = None,
metadata: dict[str, Any] | None = None,
) -> dict[str, Any]:
provider = self.get_provider(provider_id)
selected_model = model or provider.default_model
prepared_messages = list(messages)
if system_prompt:
prepared_messages = [{"role": "system", "content": system_prompt}] + prepared_messages
payload: dict[str, Any] = {
"model": selected_model,
"messages": prepared_messages,
"temperature": temperature,
}
if response_format == "json":
payload["response_format"] = {"type": "json_object"}
async with httpx.AsyncClient(timeout=RUNTIME_LLM_TIMEOUT_S) as client:
response = await client.post(provider.chat_url, json=payload, headers=provider.headers)
response.raise_for_status()
body = response.json()
choice = (body.get("choices") or [{}])[0]
message = choice.get("message") or {}
content = message.get("content")
text = self._extract_text(content)
parsed_json: dict[str, Any] | None = None
if response_format == "json":
try:
parsed_json = json.loads(text) if text else {}
except json.JSONDecodeError:
parsed_json = None
return {
"provider": provider.provider_id,
"model": selected_model,
"message": {
"role": "assistant",
"content": text,
"parsedJson": parsed_json,
},
"usage": body.get("usage"),
"metadata": metadata or {},
"completedAt": _utc_iso(),
}
async def submit_batch(
self,
*,
provider_id: str | None,
model: str | None,
job_type: str,
items: list[dict[str, Any]],
metadata: dict[str, Any] | None,
pool: Any | None = None,
actor_id: str | None = None,
) -> dict[str, Any]:
provider = self.get_provider(provider_id)
selected_model = model or provider.default_model
job_id = str(uuid.uuid4())
created_at = _utc_iso()
normalized_items = [
{
"request_id": str(item.get("request_id") or f"item_{idx+1}"),
"messages": item.get("messages") or [],
"system_prompt": item.get("system_prompt"),
"temperature": float(item.get("temperature", 0.2)),
"response_format": item.get("response_format"),
"metadata": item.get("metadata") or {},
}
for idx, item in enumerate(items)
]
job_record = {
"job_id": job_id,
"provider": provider.provider_id,
"model": selected_model,
"job_type": job_type,
"status": "queued",
"submitted_count": len(normalized_items),
"completed_count": 0,
"failed_count": 0,
"metadata": metadata or {},
"items": normalized_items,
"results": [],
"created_at": created_at,
"updated_at": created_at,
"started_at": None,
"completed_at": None,
"actor_id": actor_id,
}
self._jobs[job_id] = job_record
await self._persist_job(job_record, pool=pool)
asyncio.create_task(self._run_batch(job_id, pool=pool))
return {
"job_id": job_id,
"status": job_record["status"],
"provider": provider.provider_id,
"model": selected_model,
"submitted_count": len(normalized_items),
"created_at": created_at,
}
async def _run_batch(self, job_id: str, *, pool: Any | None = None) -> None:
job = self._jobs.get(job_id)
if not job:
return
job["status"] = "running"
job["started_at"] = _utc_iso()
job["updated_at"] = _utc_iso()
await self._persist_job(job, pool=pool)
semaphore = asyncio.Semaphore(RUNTIME_LLM_CONCURRENCY)
async def _execute_item(item: dict[str, Any]) -> dict[str, Any]:
async with semaphore:
try:
response = await self.chat(
provider_id=job["provider"],
model=job["model"],
system_prompt=item.get("system_prompt"),
messages=item.get("messages") or [],
temperature=float(item.get("temperature", 0.2)),
response_format=item.get("response_format"),
metadata=item.get("metadata") or {},
)
return {
"request_id": item["request_id"],
"status": "completed",
"response": response,
"error": None,
}
except Exception as exc: # pragma: no cover - network/runtime dependent
logger.error("runtime_llm batch item failed job=%s request=%s error=%s", job_id, item["request_id"], exc)
return {
"request_id": item["request_id"],
"status": "failed",
"response": None,
"error": str(exc),
}
results = await asyncio.gather(*[_execute_item(item) for item in job["items"]])
job["results"] = results
job["completed_count"] = sum(1 for result in results if result["status"] == "completed")
job["failed_count"] = sum(1 for result in results if result["status"] == "failed")
job["status"] = "completed" if job["failed_count"] == 0 else ("failed" if job["completed_count"] == 0 else "completed_with_errors")
job["completed_at"] = _utc_iso()
job["updated_at"] = _utc_iso()
await self._persist_job(job, pool=pool)
async def get_job(self, job_id: str, *, pool: Any | None = None) -> dict[str, Any] | None:
if job_id in self._jobs:
return self._jobs[job_id]
if pool is not None:
loaded = await self._load_job_from_db(job_id, pool=pool)
if loaded:
self._jobs[job_id] = loaded
return loaded
return None
async def list_job_results(self, job_id: str, *, pool: Any | None = None) -> list[dict[str, Any]] | None:
job = await self.get_job(job_id, pool=pool)
if not job:
return None
return list(job.get("results") or [])
async def _persist_job(self, job: dict[str, Any], *, pool: Any | None = None) -> None:
if pool is None:
return
async with pool.acquire() as conn:
await conn.execute(
"""
INSERT INTO workflow_agent_runs (
run_id,
agent_name,
trigger_type,
trigger_ref,
input_payload,
output_payload,
status,
duration_ms,
error_detail,
started_at,
completed_at
)
VALUES (
$1::uuid,
'runtime_llm',
$2,
$3,
$4::jsonb,
$5::jsonb,
$6,
$7,
$8,
$9::timestamptz,
$10::timestamptz
)
ON CONFLICT (run_id)
DO UPDATE SET
input_payload = EXCLUDED.input_payload,
output_payload = EXCLUDED.output_payload,
status = EXCLUDED.status,
duration_ms = EXCLUDED.duration_ms,
error_detail = EXCLUDED.error_detail,
started_at = EXCLUDED.started_at,
completed_at = EXCLUDED.completed_at
""",
job["job_id"],
job["job_type"],
job.get("actor_id"),
json.dumps(
{
"provider": job["provider"],
"model": job["model"],
"metadata": job.get("metadata") or {},
"items": job.get("items") or [],
}
),
json.dumps(
{
"results": job.get("results") or [],
"submitted_count": job.get("submitted_count", 0),
"completed_count": job.get("completed_count", 0),
"failed_count": job.get("failed_count", 0),
"created_at": job.get("created_at"),
"updated_at": job.get("updated_at"),
}
),
job["status"],
self._duration_ms(job.get("started_at"), job.get("completed_at")),
self._job_error_detail(job),
job.get("started_at"),
job.get("completed_at"),
)
async def _load_job_from_db(self, job_id: str, *, pool: Any) -> dict[str, Any] | None:
async with pool.acquire() as conn:
row = await conn.fetchrow(
"""
SELECT
run_id::text AS job_id,
trigger_type AS job_type,
trigger_ref AS actor_id,
input_payload,
output_payload,
status,
started_at,
completed_at
FROM workflow_agent_runs
WHERE run_id = $1::uuid AND agent_name = 'runtime_llm'
""",
job_id,
)
if not row:
return None
input_payload = dict(row["input_payload"] or {})
output_payload = dict(row["output_payload"] or {})
return {
"job_id": row["job_id"],
"provider": input_payload.get("provider"),
"model": input_payload.get("model"),
"job_type": row["job_type"],
"status": row["status"],
"submitted_count": int(output_payload.get("submitted_count", len(input_payload.get("items") or []))),
"completed_count": int(output_payload.get("completed_count", 0)),
"failed_count": int(output_payload.get("failed_count", 0)),
"metadata": input_payload.get("metadata") or {},
"items": input_payload.get("items") or [],
"results": output_payload.get("results") or [],
"created_at": output_payload.get("created_at") or (row["started_at"].isoformat() if row["started_at"] else None),
"updated_at": output_payload.get("updated_at") or (row["completed_at"].isoformat() if row["completed_at"] else None),
"started_at": row["started_at"].isoformat() if row["started_at"] else None,
"completed_at": row["completed_at"].isoformat() if row["completed_at"] else None,
"actor_id": row["actor_id"],
}
@staticmethod
def _extract_text(content: Any) -> str:
if isinstance(content, str):
return content
if isinstance(content, list):
parts: list[str] = []
for part in content:
if isinstance(part, dict):
text = part.get("text")
if isinstance(text, str):
parts.append(text)
return "\n".join(parts).strip()
return str(content or "")
@staticmethod
def _duration_ms(started_at: str | None, completed_at: str | None) -> int | None:
if not started_at or not completed_at:
return None
try:
start = datetime.fromisoformat(started_at.replace("Z", "+00:00"))
end = datetime.fromisoformat(completed_at.replace("Z", "+00:00"))
except ValueError:
return None
return max(0, int((end - start).total_seconds() * 1000))
@staticmethod
def _job_error_detail(job: dict[str, Any]) -> str | None:
failed = [result for result in job.get("results") or [] if result.get("status") == "failed"]
if not failed:
return None
return "; ".join(f"{item.get('request_id')}: {item.get('error')}" for item in failed[:5])
runtime_llm_service = RuntimeLLMService()

View File

@@ -242,11 +242,32 @@ The repo includes a Linux-side auto-deploy path for `velocity.desineuron.in`:
- [`deploy_velocity_site.sh`](infrastructure/desineuron_ingress/deploy_velocity_site.sh)
- [`install_linux_velocity_site.sh`](infrastructure/desineuron_ingress/install_linux_velocity_site.sh)
- [`install_linux_velocity_webhook.sh`](infrastructure/desineuron_ingress/install_linux_velocity_webhook.sh)
- [`desineuron-velocity-site-update.service`](infrastructure/desineuron_ingress/desineuron-velocity-site-update.service)
- [`desineuron-velocity-site-update.timer`](infrastructure/desineuron_ingress/desineuron-velocity-site-update.timer)
- [`desineuron-velocity-gitea-webhook.service`](infrastructure/desineuron_ingress/desineuron-velocity-gitea-webhook.service)
- [`gitea_velocity_webhook_receiver.py`](infrastructure/desineuron_ingress/gitea_velocity_webhook_receiver.py)
- [`velocity.desineuron.in.nginx.conf`](infrastructure/desineuron_ingress/velocity.desineuron.in.nginx.conf)
- [`api.desineuron.in.nginx.conf`](infrastructure/desineuron_ingress/api.desineuron.in.nginx.conf)
That path is designed to pull `main` from Gitea onto the Linux origin, rebuild the frontend, and republish the static site under `/var/www/velocity.desineuron.in/current`.
The deploy script is also the backend handoff point for Linux-origin production:
- resets the repo to `origin/main`
- rebuilds and republishes the frontend
- restarts `desineuron-velocity-backend`
- runs a backend health check against `http://127.0.0.1:8001/health`
- supports an optional migration hook through environment variables when schema changes are intentionally being applied
Operational triggers:
- Manual:
- `sudo systemctl start desineuron-velocity-site-update.service`
- or `sudo /usr/local/bin/deploy_velocity_site.sh`
- Automatic from Gitea:
- `https://velocity.desineuron.in/hooks/gitea/project-velocity`
- expected repo: `sagnik/Project_Velocity`
- expected branch: `main`
- shared secret stored on Linux in `/etc/desineuron-velocity-webhook.env`
## Internal Truth Sources

4
app/dist/index.html vendored
View File

@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Velocity WebOS</title>
<script type="module" crossorigin src="./assets/index-Bj2Xa_13.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-W2SBxMnB.css">
<script type="module" crossorigin src="./assets/index-C2Cn6fx_.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-CILgAuxv.css">
</head>
<body>
<div id="root"></div>

View File

@@ -1 +1 @@
{"root":["../../src/app.tsx","../../src/main.tsx","../../src/app/admin/page.tsx","../../src/app/oracle/page.tsx","../../src/components/layout/loginscreen.tsx","../../src/components/layout/notificationcenter.tsx","../../src/components/layout/sidebar.tsx","../../src/components/modules/crm.tsx","../../src/components/modules/catalyst.tsx","../../src/components/modules/catalystmarketingtab.tsx","../../src/components/modules/dashboard.tsx","../../src/components/modules/groundtruthpicker.tsx","../../src/components/modules/inventory.tsx","../../src/components/modules/oracle.tsx","../../src/components/modules/sentinel.tsx","../../src/components/modules/settings.tsx","../../src/components/modules/sentinel/perceptionplayer.tsx","../../src/components/modules/sentinel/sentinellivesession.tsx","../../src/components/oracle/leadinspector.tsx","../../src/components/oracle/pipelineview.tsx","../../src/components/sentinel/journeyriver/inspectorpanel.tsx","../../src/components/sentinel/journeyriver/riverpath.tsx","../../src/components/sentinel/journeyriver/index.tsx","../../src/components/ui/accordion.tsx","../../src/components/ui/alert-dialog.tsx","../../src/components/ui/alert.tsx","../../src/components/ui/aspect-ratio.tsx","../../src/components/ui/avatar.tsx","../../src/components/ui/badge.tsx","../../src/components/ui/breadcrumb.tsx","../../src/components/ui/button-group.tsx","../../src/components/ui/button.tsx","../../src/components/ui/calendar.tsx","../../src/components/ui/card.tsx","../../src/components/ui/carousel.tsx","../../src/components/ui/chart.tsx","../../src/components/ui/checkbox.tsx","../../src/components/ui/collapsible.tsx","../../src/components/ui/command.tsx","../../src/components/ui/context-menu.tsx","../../src/components/ui/dialog.tsx","../../src/components/ui/drawer.tsx","../../src/components/ui/dropdown-menu.tsx","../../src/components/ui/empty.tsx","../../src/components/ui/field.tsx","../../src/components/ui/form.tsx","../../src/components/ui/hover-card.tsx","../../src/components/ui/input-group.tsx","../../src/components/ui/input-otp.tsx","../../src/components/ui/input.tsx","../../src/components/ui/item.tsx","../../src/components/ui/kbd.tsx","../../src/components/ui/label.tsx","../../src/components/ui/menubar.tsx","../../src/components/ui/navigation-menu.tsx","../../src/components/ui/pagination.tsx","../../src/components/ui/popover.tsx","../../src/components/ui/progress.tsx","../../src/components/ui/radio-group.tsx","../../src/components/ui/resizable.tsx","../../src/components/ui/scroll-area.tsx","../../src/components/ui/select.tsx","../../src/components/ui/separator.tsx","../../src/components/ui/sheet.tsx","../../src/components/ui/sidebar.tsx","../../src/components/ui/skeleton.tsx","../../src/components/ui/slider.tsx","../../src/components/ui/sonner.tsx","../../src/components/ui/spinner.tsx","../../src/components/ui/switch.tsx","../../src/components/ui/table.tsx","../../src/components/ui/tabs.tsx","../../src/components/ui/textarea.tsx","../../src/components/ui/toggle-group.tsx","../../src/components/ui/toggle.tsx","../../src/components/ui/tooltip.tsx","../../src/hooks/use-mobile.ts","../../src/hooks/usecrmbootstrap.ts","../../src/hooks/usemediapipefacelandmarker.ts","../../src/hooks/usevelocitysocket.ts","../../src/lib/api.ts","../../src/lib/crmapi.ts","../../src/lib/crmmappers.ts","../../src/lib/platformmappers.ts","../../src/lib/utils.ts","../../src/lib/velocityplatformclient.ts","../../src/oracle/components/branchbar.tsx","../../src/oracle/components/canvasviewport.tsx","../../src/oracle/components/componentregistry.tsx","../../src/oracle/components/promptrail.tsx","../../src/oracle/components/rollbackconfirmmodal.tsx","../../src/oracle/components/sharemodal.tsx","../../src/oracle/components/renderers/activitystreamrenderer.tsx","../../src/oracle/components/renderers/barchartrenderer.tsx","../../src/oracle/components/renderers/errornoticerenderer.tsx","../../src/oracle/components/renderers/geomaprenderer.tsx","../../src/oracle/components/renderers/kpitilerenderer.tsx","../../src/oracle/components/renderers/linechartrenderer.tsx","../../src/oracle/components/renderers/pipelineboardrenderer.tsx","../../src/oracle/components/renderers/rendererwrapper.tsx","../../src/oracle/components/renderers/tablerenderer.tsx","../../src/oracle/components/renderers/timelinerenderer.tsx","../../src/oracle/components/review/mergereviewdrawer.tsx","../../src/oracle/hooks/useoracleexecution.ts","../../src/oracle/hooks/useoraclepage.ts","../../src/oracle/lib/oracleapiclient.ts","../../src/oracle/types/canvas.ts","../../src/store/usecurrencystore.ts","../../src/store/usemarketingstore.ts","../../src/store/usestore.ts","../../src/types/crm.ts","../../src/types/crmtypes.ts","../../src/types/index.ts","../../src/utils/curvegenerator.ts","../../src/utils/landmarkpacketencoder.ts"],"version":"5.9.3"}
{"root":["../../src/app.tsx","../../src/main.tsx","../../src/app/admin/page.tsx","../../src/app/oracle/page.tsx","../../src/components/layout/loginscreen.tsx","../../src/components/layout/notificationcenter.tsx","../../src/components/layout/sidebar.tsx","../../src/components/modules/crm.tsx","../../src/components/modules/catalyst.tsx","../../src/components/modules/catalystmarketingtab.tsx","../../src/components/modules/dashboard.tsx","../../src/components/modules/groundtruthpicker.tsx","../../src/components/modules/inventory.tsx","../../src/components/modules/oracle.tsx","../../src/components/modules/sentinel.tsx","../../src/components/modules/settings.tsx","../../src/components/modules/sentinel/perceptionplayer.tsx","../../src/components/modules/sentinel/sentinellivesession.tsx","../../src/components/oracle/leadinspector.tsx","../../src/components/oracle/pipelineview.tsx","../../src/components/sentinel/journeyriver/inspectorpanel.tsx","../../src/components/sentinel/journeyriver/riverpath.tsx","../../src/components/sentinel/journeyriver/index.tsx","../../src/components/ui/accordion.tsx","../../src/components/ui/alert-dialog.tsx","../../src/components/ui/alert.tsx","../../src/components/ui/aspect-ratio.tsx","../../src/components/ui/avatar.tsx","../../src/components/ui/badge.tsx","../../src/components/ui/breadcrumb.tsx","../../src/components/ui/button-group.tsx","../../src/components/ui/button.tsx","../../src/components/ui/calendar.tsx","../../src/components/ui/card.tsx","../../src/components/ui/carousel.tsx","../../src/components/ui/chart.tsx","../../src/components/ui/checkbox.tsx","../../src/components/ui/collapsible.tsx","../../src/components/ui/command.tsx","../../src/components/ui/context-menu.tsx","../../src/components/ui/dialog.tsx","../../src/components/ui/drawer.tsx","../../src/components/ui/dropdown-menu.tsx","../../src/components/ui/empty.tsx","../../src/components/ui/field.tsx","../../src/components/ui/form.tsx","../../src/components/ui/hover-card.tsx","../../src/components/ui/input-group.tsx","../../src/components/ui/input-otp.tsx","../../src/components/ui/input.tsx","../../src/components/ui/item.tsx","../../src/components/ui/kbd.tsx","../../src/components/ui/label.tsx","../../src/components/ui/menubar.tsx","../../src/components/ui/navigation-menu.tsx","../../src/components/ui/pagination.tsx","../../src/components/ui/popover.tsx","../../src/components/ui/progress.tsx","../../src/components/ui/radio-group.tsx","../../src/components/ui/resizable.tsx","../../src/components/ui/scroll-area.tsx","../../src/components/ui/select.tsx","../../src/components/ui/separator.tsx","../../src/components/ui/sheet.tsx","../../src/components/ui/sidebar.tsx","../../src/components/ui/skeleton.tsx","../../src/components/ui/slider.tsx","../../src/components/ui/sonner.tsx","../../src/components/ui/spinner.tsx","../../src/components/ui/switch.tsx","../../src/components/ui/table.tsx","../../src/components/ui/tabs.tsx","../../src/components/ui/textarea.tsx","../../src/components/ui/toggle-group.tsx","../../src/components/ui/toggle.tsx","../../src/components/ui/tooltip.tsx","../../src/hooks/use-mobile.ts","../../src/hooks/usecrmbootstrap.ts","../../src/hooks/usemediapipefacelandmarker.ts","../../src/hooks/usevelocitysocket.ts","../../src/lib/api.ts","../../src/lib/crmapi.ts","../../src/lib/crmmappers.ts","../../src/lib/platformmappers.ts","../../src/lib/utils.ts","../../src/lib/velocityplatformclient.ts","../../src/oracle/components/branchbar.tsx","../../src/oracle/components/canvasviewport.tsx","../../src/oracle/components/componentregistry.tsx","../../src/oracle/components/promptrail.tsx","../../src/oracle/components/rollbackconfirmmodal.tsx","../../src/oracle/components/sharemodal.tsx","../../src/oracle/components/renderers/activitystreamrenderer.tsx","../../src/oracle/components/renderers/barchartrenderer.tsx","../../src/oracle/components/renderers/errornoticerenderer.tsx","../../src/oracle/components/renderers/geomaprenderer.tsx","../../src/oracle/components/renderers/kpitilerenderer.tsx","../../src/oracle/components/renderers/linechartrenderer.tsx","../../src/oracle/components/renderers/pipelineboardrenderer.tsx","../../src/oracle/components/renderers/rendererwrapper.tsx","../../src/oracle/components/renderers/tablerenderer.tsx","../../src/oracle/components/renderers/textcanvasrenderer.tsx","../../src/oracle/components/renderers/timelinerenderer.tsx","../../src/oracle/components/review/mergereviewdrawer.tsx","../../src/oracle/hooks/useoracleexecution.ts","../../src/oracle/hooks/useoraclepage.ts","../../src/oracle/lib/oracleapiclient.ts","../../src/oracle/types/canvas.ts","../../src/store/usecurrencystore.ts","../../src/store/usemarketingstore.ts","../../src/store/usestore.ts","../../src/types/crm.ts","../../src/types/crmtypes.ts","../../src/types/index.ts","../../src/utils/curvegenerator.ts","../../src/utils/landmarkpacketencoder.ts"],"version":"5.9.3"}

View File

@@ -1,24 +1,24 @@
"use client";
import {
createSlot
} from "./chunk-5HUACAZ7.js";
import {
useCallbackRef,
useLayoutEffect2
} from "./chunk-J4JAFMOP.js";
import {
createSlot
} from "./chunk-YWBEB5PG.js";
import "./chunk-2VUH7NEY.js";
import {
require_shim
} from "./chunk-TXHHHGR3.js";
} from "./chunk-GRXJTWBV.js";
import "./chunk-HPBHRBIF.js";
import {
require_react_dom
} from "./chunk-YF4B4G2L.js";
} from "./chunk-YLZ34CCM.js";
import {
require_shim
} from "./chunk-642Z5WD3.js";
import {
require_jsx_runtime
} from "./chunk-2YVA4HRZ.js";
} from "./chunk-USXRE7Q2.js";
import {
require_react
} from "./chunk-WUR7D6NS.js";
} from "./chunk-ZNKPWGXJ.js";
import {
__toESM
} from "./chunk-G3PMV62Z.js";

View File

@@ -2,20 +2,20 @@
import {
useCallbackRef,
useLayoutEffect2
} from "./chunk-J4JAFMOP.js";
} from "./chunk-GRXJTWBV.js";
import {
composeRefs,
useComposedRefs
} from "./chunk-2VUH7NEY.js";
} from "./chunk-HPBHRBIF.js";
import {
require_react_dom
} from "./chunk-YF4B4G2L.js";
} from "./chunk-YLZ34CCM.js";
import {
require_jsx_runtime
} from "./chunk-2YVA4HRZ.js";
} from "./chunk-USXRE7Q2.js";
import {
require_react
} from "./chunk-WUR7D6NS.js";
} from "./chunk-ZNKPWGXJ.js";
import {
__toESM
} from "./chunk-G3PMV62Z.js";

File diff suppressed because one or more lines are too long

View File

@@ -3,10 +3,10 @@ import {
Slottable,
createSlot,
createSlottable
} from "./chunk-YWBEB5PG.js";
import "./chunk-2VUH7NEY.js";
import "./chunk-2YVA4HRZ.js";
import "./chunk-WUR7D6NS.js";
} from "./chunk-5HUACAZ7.js";
import "./chunk-HPBHRBIF.js";
import "./chunk-USXRE7Q2.js";
import "./chunk-ZNKPWGXJ.js";
import "./chunk-G3PMV62Z.js";
export {
Slot as Root,

View File

@@ -1,12 +1,19 @@
import {
create
} from "./chunk-7GZ4CI6Q.js";
import {
subscribeWithSelector
} from "./chunk-O4L7C4YS.js";
} from "./chunk-XGWIEMTH.js";
import {
create
} from "./chunk-QJTQF54Q.js";
import {
Events
} from "./chunk-OAEA5FZL.js";
import {
require_client
} from "./chunk-2NWYL6R2.js";
import {
_extends
} from "./chunk-H4GSM2WL.js";
import "./chunk-YLZ34CCM.js";
import {
addAfterEffect,
addEffect,
@@ -22,8 +29,8 @@ import {
useInstanceHandle,
useLoader,
useThree
} from "./chunk-5ESDTKMP.js";
import "./chunk-NJ4V5H3P.js";
} from "./chunk-CSHY5MMV.js";
import "./chunk-LTNRPUSL.js";
import {
AddEquation,
AdditiveBlending,
@@ -217,20 +224,13 @@ import {
WebGLRenderer,
WireframeGeometry,
ZeroFactor
} from "./chunk-L3Z576C2.js";
import {
require_client
} from "./chunk-6MXH2QM6.js";
import "./chunk-GUQHL3N7.js";
import {
_extends
} from "./chunk-EQCCHGRT.js";
import "./chunk-TXHHHGR3.js";
import "./chunk-YF4B4G2L.js";
import "./chunk-2YVA4HRZ.js";
} from "./chunk-INS7YHTD.js";
import "./chunk-QURGMCZB.js";
import "./chunk-642Z5WD3.js";
import "./chunk-USXRE7Q2.js";
import {
require_react
} from "./chunk-WUR7D6NS.js";
} from "./chunk-ZNKPWGXJ.js";
import {
__commonJS,
__toESM

File diff suppressed because one or more lines are too long

View File

@@ -28,13 +28,13 @@ import {
useLoader,
useStore,
useThree
} from "./chunk-5ESDTKMP.js";
import "./chunk-NJ4V5H3P.js";
import "./chunk-L3Z576C2.js";
import "./chunk-GUQHL3N7.js";
import "./chunk-TXHHHGR3.js";
import "./chunk-2YVA4HRZ.js";
import "./chunk-WUR7D6NS.js";
} from "./chunk-CSHY5MMV.js";
import "./chunk-LTNRPUSL.js";
import "./chunk-INS7YHTD.js";
import "./chunk-QURGMCZB.js";
import "./chunk-642Z5WD3.js";
import "./chunk-USXRE7Q2.js";
import "./chunk-ZNKPWGXJ.js";
import "./chunk-G3PMV62Z.js";
export {
Canvas,

View File

@@ -1,133 +1,133 @@
{
"hash": "4594f192",
"configHash": "1dd3b956",
"lockfileHash": "e8550e82",
"browserHash": "7e7e8c10",
"hash": "9ed426b5",
"configHash": "6a55a817",
"lockfileHash": "cbf147e9",
"browserHash": "a13f5201",
"optimized": {
"react": {
"src": "../../react/index.js",
"file": "react.js",
"fileHash": "bc0c1f26",
"fileHash": "44c1ad00",
"needsInterop": true
},
"react-dom": {
"src": "../../react-dom/index.js",
"file": "react-dom.js",
"fileHash": "36a8d9c0",
"fileHash": "09fbf9a4",
"needsInterop": true
},
"react/jsx-dev-runtime": {
"src": "../../react/jsx-dev-runtime.js",
"file": "react_jsx-dev-runtime.js",
"fileHash": "3d8f6460",
"fileHash": "ce2da90b",
"needsInterop": true
},
"react/jsx-runtime": {
"src": "../../react/jsx-runtime.js",
"file": "react_jsx-runtime.js",
"fileHash": "6f4aca26",
"fileHash": "52be981b",
"needsInterop": true
},
"@radix-ui/react-avatar": {
"src": "../../@radix-ui/react-avatar/dist/index.mjs",
"file": "@radix-ui_react-avatar.js",
"fileHash": "2a702dd2",
"fileHash": "63b564be",
"needsInterop": false
},
"@radix-ui/react-dropdown-menu": {
"src": "../../@radix-ui/react-dropdown-menu/dist/index.mjs",
"file": "@radix-ui_react-dropdown-menu.js",
"fileHash": "a5efb9bf",
"fileHash": "b9686e90",
"needsInterop": false
},
"@radix-ui/react-slot": {
"src": "../../@radix-ui/react-slot/dist/index.mjs",
"file": "@radix-ui_react-slot.js",
"fileHash": "986d9c0d",
"fileHash": "417c3a07",
"needsInterop": false
},
"@react-three/drei": {
"src": "../../@react-three/drei/index.js",
"file": "@react-three_drei.js",
"fileHash": "6cd60875",
"fileHash": "b25127e3",
"needsInterop": false
},
"@react-three/fiber": {
"src": "../../@react-three/fiber/dist/react-three-fiber.esm.js",
"file": "@react-three_fiber.js",
"fileHash": "27a7d4df",
"fileHash": "22a2309e",
"needsInterop": false
},
"class-variance-authority": {
"src": "../../class-variance-authority/dist/index.mjs",
"file": "class-variance-authority.js",
"fileHash": "b0c32b93",
"fileHash": "6e6c6fd0",
"needsInterop": false
},
"clsx": {
"src": "../../clsx/dist/clsx.mjs",
"file": "clsx.js",
"fileHash": "c855e729",
"fileHash": "eb68424d",
"needsInterop": false
},
"framer-motion": {
"src": "../../framer-motion/dist/es/index.mjs",
"file": "framer-motion.js",
"fileHash": "e0841dfa",
"fileHash": "1cbcab3b",
"needsInterop": false
},
"lucide-react": {
"src": "../../lucide-react/dist/esm/lucide-react.js",
"file": "lucide-react.js",
"fileHash": "4d79a586",
"fileHash": "6dded310",
"needsInterop": false
},
"react-dom/client": {
"src": "../../react-dom/client.js",
"file": "react-dom_client.js",
"fileHash": "2e02376b",
"fileHash": "c3a7edc3",
"needsInterop": true
},
"react-router-dom": {
"src": "../../react-router-dom/dist/index.mjs",
"file": "react-router-dom.js",
"fileHash": "bd4cf4c4",
"fileHash": "e91f778e",
"needsInterop": false
},
"recharts": {
"src": "../../recharts/es6/index.js",
"file": "recharts.js",
"fileHash": "b44545db",
"fileHash": "d7f9dad1",
"needsInterop": false
},
"sonner": {
"src": "../../sonner/dist/index.mjs",
"file": "sonner.js",
"fileHash": "02632b99",
"fileHash": "8433c1a9",
"needsInterop": false
},
"tailwind-merge": {
"src": "../../tailwind-merge/dist/bundle-mjs.mjs",
"file": "tailwind-merge.js",
"fileHash": "ab22bcc4",
"fileHash": "772f1bbd",
"needsInterop": false
},
"three": {
"src": "../../three/build/three.module.js",
"file": "three.js",
"fileHash": "43012f83",
"fileHash": "490e5c00",
"needsInterop": false
},
"zustand": {
"src": "../../zustand/esm/index.mjs",
"file": "zustand.js",
"fileHash": "dbfba0e2",
"fileHash": "315f8e85",
"needsInterop": false
},
"zustand/middleware": {
"src": "../../zustand/esm/middleware.mjs",
"file": "zustand_middleware.js",
"fileHash": "e524c2dc",
"fileHash": "2563a89b",
"needsInterop": false
}
},
@@ -135,56 +135,56 @@
"hls-Q6LDPZPT": {
"file": "hls-Q6LDPZPT.js"
},
"chunk-U7P2NEEE": {
"file": "chunk-U7P2NEEE.js"
"chunk-XGWIEMTH": {
"file": "chunk-XGWIEMTH.js"
},
"chunk-J4JAFMOP": {
"file": "chunk-J4JAFMOP.js"
},
"chunk-YWBEB5PG": {
"file": "chunk-YWBEB5PG.js"
},
"chunk-2VUH7NEY": {
"file": "chunk-2VUH7NEY.js"
},
"chunk-7GZ4CI6Q": {
"file": "chunk-7GZ4CI6Q.js"
},
"chunk-O4L7C4YS": {
"file": "chunk-O4L7C4YS.js"
"chunk-QJTQF54Q": {
"file": "chunk-QJTQF54Q.js"
},
"chunk-OAEA5FZL": {
"file": "chunk-OAEA5FZL.js"
},
"chunk-5ESDTKMP": {
"file": "chunk-5ESDTKMP.js"
"chunk-2NWYL6R2": {
"file": "chunk-2NWYL6R2.js"
},
"chunk-NJ4V5H3P": {
"file": "chunk-NJ4V5H3P.js"
"chunk-H4GSM2WL": {
"file": "chunk-H4GSM2WL.js"
},
"chunk-L3Z576C2": {
"file": "chunk-L3Z576C2.js"
"chunk-5HUACAZ7": {
"file": "chunk-5HUACAZ7.js"
},
"chunk-6MXH2QM6": {
"file": "chunk-6MXH2QM6.js"
"chunk-GRXJTWBV": {
"file": "chunk-GRXJTWBV.js"
},
"chunk-GUQHL3N7": {
"file": "chunk-GUQHL3N7.js"
"chunk-HPBHRBIF": {
"file": "chunk-HPBHRBIF.js"
},
"chunk-EQCCHGRT": {
"file": "chunk-EQCCHGRT.js"
"chunk-YLZ34CCM": {
"file": "chunk-YLZ34CCM.js"
},
"chunk-TXHHHGR3": {
"file": "chunk-TXHHHGR3.js"
"chunk-CSHY5MMV": {
"file": "chunk-CSHY5MMV.js"
},
"chunk-YF4B4G2L": {
"file": "chunk-YF4B4G2L.js"
"chunk-LTNRPUSL": {
"file": "chunk-LTNRPUSL.js"
},
"chunk-2YVA4HRZ": {
"file": "chunk-2YVA4HRZ.js"
"chunk-INS7YHTD": {
"file": "chunk-INS7YHTD.js"
},
"chunk-WUR7D6NS": {
"file": "chunk-WUR7D6NS.js"
"chunk-QURGMCZB": {
"file": "chunk-QURGMCZB.js"
},
"chunk-642Z5WD3": {
"file": "chunk-642Z5WD3.js"
},
"chunk-USXRE7Q2": {
"file": "chunk-USXRE7Q2.js"
},
"chunk-ZNKPWGXJ": {
"file": "chunk-ZNKPWGXJ.js"
},
"chunk-U7P2NEEE": {
"file": "chunk-U7P2NEEE.js"
},
"chunk-G3PMV62Z": {
"file": "chunk-G3PMV62Z.js"

View File

@@ -1,7 +1,7 @@
{
"version": 3,
"sources": ["../../class-variance-authority/dist/index.mjs"],
"sourcesContent": ["/**\n * Copyright 2022 Joe Bell. All rights reserved.\n *\n * This file is licensed to you under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with the\n * License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */ import { clsx } from \"clsx\";\nconst falsyToString = (value)=>typeof value === \"boolean\" ? `${value}` : value === 0 ? \"0\" : value;\nexport const cx = clsx;\nexport const cva = (base, config)=>(props)=>{\n var _config_compoundVariants;\n if ((config === null || config === void 0 ? void 0 : config.variants) == null) return cx(base, props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);\n const { variants, defaultVariants } = config;\n const getVariantClassNames = Object.keys(variants).map((variant)=>{\n const variantProp = props === null || props === void 0 ? void 0 : props[variant];\n const defaultVariantProp = defaultVariants === null || defaultVariants === void 0 ? void 0 : defaultVariants[variant];\n if (variantProp === null) return null;\n const variantKey = falsyToString(variantProp) || falsyToString(defaultVariantProp);\n return variants[variant][variantKey];\n });\n const propsWithoutUndefined = props && Object.entries(props).reduce((acc, param)=>{\n let [key, value] = param;\n if (value === undefined) {\n return acc;\n }\n acc[key] = value;\n return acc;\n }, {});\n const getCompoundVariantClassNames = config === null || config === void 0 ? void 0 : (_config_compoundVariants = config.compoundVariants) === null || _config_compoundVariants === void 0 ? void 0 : _config_compoundVariants.reduce((acc, param)=>{\n let { class: cvClass, className: cvClassName, ...compoundVariantOptions } = param;\n return Object.entries(compoundVariantOptions).every((param)=>{\n let [key, value] = param;\n return Array.isArray(value) ? value.includes({\n ...defaultVariants,\n ...propsWithoutUndefined\n }[key]) : ({\n ...defaultVariants,\n ...propsWithoutUndefined\n })[key] === value;\n }) ? [\n ...acc,\n cvClass,\n cvClassName\n ] : acc;\n }, []);\n return cx(base, getVariantClassNames, getCompoundVariantClassNames, props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);\n };\n\n"],
"sourcesContent": ["/**\r\n * Copyright 2022 Joe Bell. All rights reserved.\r\n *\r\n * This file is licensed to you under the Apache License, Version 2.0\r\n * (the \"License\"); you may not use this file except in compliance with the\r\n * License. You may obtain a copy of the License at\r\n *\r\n * http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\r\n * WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or implied. See the\r\n * License for the specific language governing permissions and limitations under\r\n * the License.\r\n */ import { clsx } from \"clsx\";\r\nconst falsyToString = (value)=>typeof value === \"boolean\" ? `${value}` : value === 0 ? \"0\" : value;\r\nexport const cx = clsx;\r\nexport const cva = (base, config)=>(props)=>{\r\n var _config_compoundVariants;\r\n if ((config === null || config === void 0 ? void 0 : config.variants) == null) return cx(base, props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);\r\n const { variants, defaultVariants } = config;\r\n const getVariantClassNames = Object.keys(variants).map((variant)=>{\r\n const variantProp = props === null || props === void 0 ? void 0 : props[variant];\r\n const defaultVariantProp = defaultVariants === null || defaultVariants === void 0 ? void 0 : defaultVariants[variant];\r\n if (variantProp === null) return null;\r\n const variantKey = falsyToString(variantProp) || falsyToString(defaultVariantProp);\r\n return variants[variant][variantKey];\r\n });\r\n const propsWithoutUndefined = props && Object.entries(props).reduce((acc, param)=>{\r\n let [key, value] = param;\r\n if (value === undefined) {\r\n return acc;\r\n }\r\n acc[key] = value;\r\n return acc;\r\n }, {});\r\n const getCompoundVariantClassNames = config === null || config === void 0 ? void 0 : (_config_compoundVariants = config.compoundVariants) === null || _config_compoundVariants === void 0 ? void 0 : _config_compoundVariants.reduce((acc, param)=>{\r\n let { class: cvClass, className: cvClassName, ...compoundVariantOptions } = param;\r\n return Object.entries(compoundVariantOptions).every((param)=>{\r\n let [key, value] = param;\r\n return Array.isArray(value) ? value.includes({\r\n ...defaultVariants,\r\n ...propsWithoutUndefined\r\n }[key]) : ({\r\n ...defaultVariants,\r\n ...propsWithoutUndefined\r\n })[key] === value;\r\n }) ? [\r\n ...acc,\r\n cvClass,\r\n cvClassName\r\n ] : acc;\r\n }, []);\r\n return cx(base, getVariantClassNames, getCompoundVariantClassNames, props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);\r\n };\r\n\r\n"],
"mappings": ";;;;;;AAeA,IAAM,gBAAgB,CAAC,UAAQ,OAAO,UAAU,YAAY,GAAG,KAAK,KAAK,UAAU,IAAI,MAAM;AACtF,IAAM,KAAK;AACX,IAAM,MAAM,CAAC,MAAM,WAAS,CAAC,UAAQ;AACpC,MAAI;AACJ,OAAK,WAAW,QAAQ,WAAW,SAAS,SAAS,OAAO,aAAa,KAAM,QAAO,GAAG,MAAM,UAAU,QAAQ,UAAU,SAAS,SAAS,MAAM,OAAO,UAAU,QAAQ,UAAU,SAAS,SAAS,MAAM,SAAS;AACvN,QAAM,EAAE,UAAU,gBAAgB,IAAI;AACtC,QAAM,uBAAuB,OAAO,KAAK,QAAQ,EAAE,IAAI,CAAC,YAAU;AAC9D,UAAM,cAAc,UAAU,QAAQ,UAAU,SAAS,SAAS,MAAM,OAAO;AAC/E,UAAM,qBAAqB,oBAAoB,QAAQ,oBAAoB,SAAS,SAAS,gBAAgB,OAAO;AACpH,QAAI,gBAAgB,KAAM,QAAO;AACjC,UAAM,aAAa,cAAc,WAAW,KAAK,cAAc,kBAAkB;AACjF,WAAO,SAAS,OAAO,EAAE,UAAU;AAAA,EACvC,CAAC;AACD,QAAM,wBAAwB,SAAS,OAAO,QAAQ,KAAK,EAAE,OAAO,CAAC,KAAK,UAAQ;AAC9E,QAAI,CAAC,KAAK,KAAK,IAAI;AACnB,QAAI,UAAU,QAAW;AACrB,aAAO;AAAA,IACX;AACA,QAAI,GAAG,IAAI;AACX,WAAO;AAAA,EACX,GAAG,CAAC,CAAC;AACL,QAAM,+BAA+B,WAAW,QAAQ,WAAW,SAAS,UAAU,2BAA2B,OAAO,sBAAsB,QAAQ,6BAA6B,SAAS,SAAS,yBAAyB,OAAO,CAAC,KAAK,UAAQ;AAC/O,QAAI,EAAE,OAAO,SAAS,WAAW,aAAa,GAAG,uBAAuB,IAAI;AAC5E,WAAO,OAAO,QAAQ,sBAAsB,EAAE,MAAM,CAACA,WAAQ;AACzD,UAAI,CAAC,KAAK,KAAK,IAAIA;AACnB,aAAO,MAAM,QAAQ,KAAK,IAAI,MAAM,SAAS;AAAA,QACzC,GAAG;AAAA,QACH,GAAG;AAAA,MACP,EAAE,GAAG,CAAC,IAAK;AAAA,QACP,GAAG;AAAA,QACH,GAAG;AAAA,MACP,EAAG,GAAG,MAAM;AAAA,IAChB,CAAC,IAAI;AAAA,MACD,GAAG;AAAA,MACH;AAAA,MACA;AAAA,IACJ,IAAI;AAAA,EACR,GAAG,CAAC,CAAC;AACL,SAAO,GAAG,MAAM,sBAAsB,8BAA8B,UAAU,QAAQ,UAAU,SAAS,SAAS,MAAM,OAAO,UAAU,QAAQ,UAAU,SAAS,SAAS,MAAM,SAAS;AAChM;",
"names": ["param"]
}

View File

@@ -1,9 +1,9 @@
import {
require_jsx_runtime
} from "./chunk-2YVA4HRZ.js";
} from "./chunk-USXRE7Q2.js";
import {
require_react
} from "./chunk-WUR7D6NS.js";
} from "./chunk-ZNKPWGXJ.js";
import {
__commonJS,
__export,

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
import {
require_react
} from "./chunk-WUR7D6NS.js";
} from "./chunk-ZNKPWGXJ.js";
import {
__export,
__toESM

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
import {
require_react_dom
} from "./chunk-YF4B4G2L.js";
import "./chunk-WUR7D6NS.js";
} from "./chunk-YLZ34CCM.js";
import "./chunk-ZNKPWGXJ.js";
import "./chunk-G3PMV62Z.js";
export default require_react_dom();

View File

@@ -1,8 +1,8 @@
import {
require_client
} from "./chunk-6MXH2QM6.js";
import "./chunk-GUQHL3N7.js";
import "./chunk-YF4B4G2L.js";
import "./chunk-WUR7D6NS.js";
} from "./chunk-2NWYL6R2.js";
import "./chunk-YLZ34CCM.js";
import "./chunk-QURGMCZB.js";
import "./chunk-ZNKPWGXJ.js";
import "./chunk-G3PMV62Z.js";
export default require_client();

View File

@@ -1,5 +1,5 @@
import {
require_react
} from "./chunk-WUR7D6NS.js";
} from "./chunk-ZNKPWGXJ.js";
import "./chunk-G3PMV62Z.js";
export default require_react();

View File

@@ -1,6 +1,6 @@
import {
require_react
} from "./chunk-WUR7D6NS.js";
} from "./chunk-ZNKPWGXJ.js";
import {
__commonJS
} from "./chunk-G3PMV62Z.js";

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
import {
require_jsx_runtime
} from "./chunk-2YVA4HRZ.js";
import "./chunk-WUR7D6NS.js";
} from "./chunk-USXRE7Q2.js";
import "./chunk-ZNKPWGXJ.js";
import "./chunk-G3PMV62Z.js";
export default require_jsx_runtime();

View File

@@ -1,15 +1,15 @@
import {
_extends
} from "./chunk-H4GSM2WL.js";
import {
require_react_dom
} from "./chunk-YLZ34CCM.js";
import {
require_react
} from "./chunk-ZNKPWGXJ.js";
import {
clsx_default
} from "./chunk-U7P2NEEE.js";
import {
_extends
} from "./chunk-EQCCHGRT.js";
import {
require_react_dom
} from "./chunk-YF4B4G2L.js";
import {
require_react
} from "./chunk-WUR7D6NS.js";
import {
__commonJS,
__export,

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More