Oracle Canvas Component Schema and Qwen 3.6 integration #31
209
.Agent Context/Oracle Canvas Codebook Production Truth.md
Normal file
209
.Agent Context/Oracle Canvas Codebook Production Truth.md
Normal 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
|
||||
@@ -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
|
||||
@@ -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** — 4–6 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*
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
BIN
.oracle_deploy_stage.tar
Normal file
BIN
.oracle_deploy_stage.tar
Normal file
Binary file not shown.
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
489
.oracle_deploy_stage/app/src/oracle/types/canvas.ts
Normal file
489
.oracle_deploy_stage/app/src/oracle/types/canvas.ts
Normal 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>;
|
||||
}
|
||||
106
.oracle_deploy_stage/backend/api/routes_oracle.py
Normal file
106
.oracle_deploy_stage/backend/api/routes_oracle.py
Normal 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}
|
||||
404
.oracle_deploy_stage/backend/api/routes_oracle_templates.py
Normal file
404
.oracle_deploy_stage/backend/api/routes_oracle_templates.py
Normal 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"]),
|
||||
}
|
||||
140
.oracle_deploy_stage/backend/api/routes_runtime_llm.py
Normal file
140
.oracle_deploy_stage/backend/api/routes_runtime_llm.py
Normal 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)}}
|
||||
411
.oracle_deploy_stage/backend/main.py
Normal file
411
.oracle_deploy_stage/backend/main.py
Normal 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(),
|
||||
}
|
||||
|
||||
340
.oracle_deploy_stage/backend/oracle/codebook_service.py
Normal file
340
.oracle_deploy_stage/backend/oracle/codebook_service.py
Normal 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()
|
||||
322
.oracle_deploy_stage/backend/oracle/data_access_gateway.py
Normal file
322
.oracle_deploy_stage/backend/oracle/data_access_gateway.py
Normal 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()
|
||||
153597
.oracle_deploy_stage/backend/oracle/oracle_runtime_codebook_merged.json
Normal file
153597
.oracle_deploy_stage/backend/oracle/oracle_runtime_codebook_merged.json
Normal file
File diff suppressed because it is too large
Load Diff
876
.oracle_deploy_stage/backend/oracle/prompt_orchestrator.py
Normal file
876
.oracle_deploy_stage/backend/oracle/prompt_orchestrator.py
Normal 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()
|
||||
458
.oracle_deploy_stage/backend/oracle/router_v1.py
Normal file
458
.oracle_deploy_stage/backend/oracle/router_v1.py
Normal 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 ───────────────────────────────────────────────────
|
||||
|
||||
@@ -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()
|
||||
461
.oracle_deploy_stage/backend/services/runtime_llm_service.py
Normal file
461
.oracle_deploy_stage/backend/services/runtime_llm_service.py
Normal 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()
|
||||
21
README.md
21
README.md
@@ -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
4
app/dist/index.html
vendored
@@ -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>
|
||||
|
||||
2
app/node_modules/.tmp/tsconfig.app.tsbuildinfo
generated
vendored
2
app/node_modules/.tmp/tsconfig.app.tsbuildinfo
generated
vendored
@@ -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"}
|
||||
22
app/node_modules/.vite/deps/@radix-ui_react-avatar.js
generated
vendored
22
app/node_modules/.vite/deps/@radix-ui_react-avatar.js
generated
vendored
@@ -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";
|
||||
|
||||
10
app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js
generated
vendored
10
app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js
generated
vendored
@@ -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";
|
||||
|
||||
2
app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js.map
generated
vendored
2
app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js.map
generated
vendored
File diff suppressed because one or more lines are too long
8
app/node_modules/.vite/deps/@radix-ui_react-slot.js
generated
vendored
8
app/node_modules/.vite/deps/@radix-ui_react-slot.js
generated
vendored
@@ -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,
|
||||
|
||||
36
app/node_modules/.vite/deps/@react-three_drei.js
generated
vendored
36
app/node_modules/.vite/deps/@react-three_drei.js
generated
vendored
@@ -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
|
||||
|
||||
2
app/node_modules/.vite/deps/@react-three_drei.js.map
generated
vendored
2
app/node_modules/.vite/deps/@react-three_drei.js.map
generated
vendored
File diff suppressed because one or more lines are too long
14
app/node_modules/.vite/deps/@react-three_fiber.js
generated
vendored
14
app/node_modules/.vite/deps/@react-three_fiber.js
generated
vendored
@@ -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,
|
||||
|
||||
122
app/node_modules/.vite/deps/_metadata.json
generated
vendored
122
app/node_modules/.vite/deps/_metadata.json
generated
vendored
@@ -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"
|
||||
|
||||
2
app/node_modules/.vite/deps/class-variance-authority.js.map
generated
vendored
2
app/node_modules/.vite/deps/class-variance-authority.js.map
generated
vendored
@@ -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"]
|
||||
}
|
||||
|
||||
4
app/node_modules/.vite/deps/framer-motion.js
generated
vendored
4
app/node_modules/.vite/deps/framer-motion.js
generated
vendored
@@ -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,
|
||||
|
||||
2
app/node_modules/.vite/deps/framer-motion.js.map
generated
vendored
2
app/node_modules/.vite/deps/framer-motion.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2
app/node_modules/.vite/deps/lucide-react.js
generated
vendored
2
app/node_modules/.vite/deps/lucide-react.js
generated
vendored
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
require_react
|
||||
} from "./chunk-WUR7D6NS.js";
|
||||
} from "./chunk-ZNKPWGXJ.js";
|
||||
import {
|
||||
__export,
|
||||
__toESM
|
||||
|
||||
2
app/node_modules/.vite/deps/lucide-react.js.map
generated
vendored
2
app/node_modules/.vite/deps/lucide-react.js.map
generated
vendored
File diff suppressed because one or more lines are too long
4
app/node_modules/.vite/deps/react-dom.js
generated
vendored
4
app/node_modules/.vite/deps/react-dom.js
generated
vendored
@@ -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();
|
||||
|
||||
8
app/node_modules/.vite/deps/react-dom_client.js
generated
vendored
8
app/node_modules/.vite/deps/react-dom_client.js
generated
vendored
@@ -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();
|
||||
|
||||
2
app/node_modules/.vite/deps/react.js
generated
vendored
2
app/node_modules/.vite/deps/react.js
generated
vendored
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
require_react
|
||||
} from "./chunk-WUR7D6NS.js";
|
||||
} from "./chunk-ZNKPWGXJ.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export default require_react();
|
||||
|
||||
2
app/node_modules/.vite/deps/react_jsx-dev-runtime.js
generated
vendored
2
app/node_modules/.vite/deps/react_jsx-dev-runtime.js
generated
vendored
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
require_react
|
||||
} from "./chunk-WUR7D6NS.js";
|
||||
} from "./chunk-ZNKPWGXJ.js";
|
||||
import {
|
||||
__commonJS
|
||||
} from "./chunk-G3PMV62Z.js";
|
||||
|
||||
2
app/node_modules/.vite/deps/react_jsx-dev-runtime.js.map
generated
vendored
2
app/node_modules/.vite/deps/react_jsx-dev-runtime.js.map
generated
vendored
File diff suppressed because one or more lines are too long
4
app/node_modules/.vite/deps/react_jsx-runtime.js
generated
vendored
4
app/node_modules/.vite/deps/react_jsx-runtime.js
generated
vendored
@@ -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();
|
||||
|
||||
18
app/node_modules/.vite/deps/recharts.js
generated
vendored
18
app/node_modules/.vite/deps/recharts.js
generated
vendored
@@ -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,
|
||||
|
||||
2
app/node_modules/.vite/deps/recharts.js.map
generated
vendored
2
app/node_modules/.vite/deps/recharts.js.map
generated
vendored
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
Reference in New Issue
Block a user