NodeXR is an XR collaborative design system in which user utterances are converted into graph nodes, image-generation inputs, 2D assets, and 3D assets.
The backend is built with:
- FastAPI
- SQLAlchemy
- PostgreSQL with pgvector
- WebSocket
- MinIO
- OpenAI and Gemini APIs
- Alembic
The graph domain contains nodes and edges used by both REST APIs and WebSocket events.
A graph mutation can affect:
- nodes
- edges
- descendant nodes
- graph snapshots
- image-generation inputs
- WebSocket events
Do not treat graph operations as isolated CRUD operations.
The main backend source code is under app.
app
├── ai
│ └── prompts
│ └── keyword_prompt.py
├── alembic
│ ├── env.py
│ ├── script.py.mako
│ └── versions
├── alembic.ini
├── api
│ ├── feature.py
│ ├── generation.py
│ ├── history.py
│ ├── room.py
│ ├── utterance.py
│ └── ws_room_event.py
├── converter
│ └── graph_converter.py
├── core
│ ├── config.py
│ ├── logger.py
│ ├── minio.py
│ ├── performance.py
│ ├── response
│ │ ├── code.py
│ │ ├── exception_handler.py
│ │ ├── exceptions.py
│ │ ├── response.py
│ │ ├── ws_exception_handler.py
│ │ ├── ws_exceptions.py
│ │ └── ws_response.py
│ ├── security.py
│ ├── startup.py
│ ├── validators.py
│ └── ws_utils.py
├── db
│ ├── base.py
│ ├── init_db.py
│ └── session.py
├── main.py
├── model
│ ├── __init__.py
│ ├── agent.py
│ ├── asset.py
│ ├── enum.py
│ ├── feature.py
│ ├── graph.py
│ ├── memory.py
│ ├── reference.py
│ └── room.py
├── repository
│ ├── asset_repository.py
│ ├── feature_repository.py
│ ├── graph_repository.py
│ ├── room_repository.py
│ └── utterance_repository.py
├── schema
│ ├── feature
│ │ ├── request.py
│ │ └── response.py
│ ├── generation
│ │ ├── generation_result.py
│ │ ├── node_keyword_response.py
│ │ ├── request.py
│ │ └── ws_event_generation_payload.py
│ ├── graph
│ │ ├── response.py
│ │ ├── ws_event_edge_payload.py
│ │ └── ws_event_node_payload.py
│ ├── guide
│ ├── history
│ │ ├── request.py
│ │ └── response.py
│ ├── room
│ │ ├── request.py
│ │ └── response.py
│ ├── utterance
│ │ ├── request.py
│ │ └── ws_event_utterance_payload.py
│ └── websocket
│ └── ws_event.py
└── service
├── agent
├── feature
│ └── feature_service.py
├── generation
│ ├── feature_prompt_context_builder.py
│ ├── feature_prompt_generation_service.py
│ ├── gemini_image_client.py
│ ├── image_2d_feature_generation_service.py
│ ├── image_2d_generation_service.py
│ ├── minio_asset_storage.py
│ ├── model_3d_generation_service.py
│ ├── openai_prompt_client.py
│ ├── prompt_context_builder.py
│ └── prompt_generation_service.py
├── graph
│ ├── graph_build_service.py
│ └── graph_interaction_service.py
├── history
│ └── history_service.py
├── room
│ └── room_service.py
├── utterance
│ ├── auto_utterance_service.py
│ ├── button_utterance_service.py
│ ├── embedding_service.py
│ ├── keyword_service.py
│ └── text_preprocess_service.py
└── websocket
└── connection_manager.py
Preserve this structure unless a requested feature clearly requires a new file.
Do not introduce a completely new architectural layer without explaining why the existing structure cannot support the feature.
Before modifying code, inspect the relevant existing implementation.
For graph-related work, inspect at minimum:
app/model/graph.pyapp/model/enum.pyapp/repository/graph_repository.pyapp/service/graph/graph_build_service.pyapp/service/graph/graph_interaction_service.pyapp/api/ws_room_event.pyapp/schema/graph/response.pyapp/schema/graph/ws_event_node_payload.pyapp/schema/graph/ws_event_edge_payload.pyapp/schema/websocket/ws_event.pyapp/converter/graph_converter.py
For room validation or authorization, inspect:
app/model/room.pyapp/repository/room_repository.pyapp/service/room/room_service.pyapp/api/room.pyapp/core/security.pyapp/core/validators.py
For API response and error handling, inspect:
app/core/response/code.pyapp/core/response/response.pyapp/core/response/exceptions.pyapp/core/response/exception_handler.pyapp/core/response/ws_exceptions.pyapp/core/response/ws_exception_handler.pyapp/core/response/ws_response.py
For database and transaction conventions, inspect:
app/db/session.pyapp/db/base.py- existing service and repository methods that call
flush,commit,refresh, orrollback
Do not assume class names, method names, schemas, enum values, or database columns before inspecting the actual code.
Follow the existing separation between API, service, repository, schema, model, and converter code.
API modules are responsible for:
- declaring routes
- receiving HTTP or WebSocket requests
- parsing request schemas
- injecting dependencies
- invoking services
- returning project-standard responses
Do not place SQLAlchemy queries or graph mutation business logic directly in API modules.
REST graph APIs should be added to an existing appropriate API module or a narrowly scoped new module under app/api.
Do not put REST CRUD logic inside ws_room_event.py.
Service modules are responsible for:
- business validation
- transaction orchestration
- coordinating multiple repository operations
- graph mutation workflows
- graph snapshot rebuilding
- coordinating WebSocket event publication
- coordinating external clients when required
Graph mutation logic should primarily be implemented or reused under:
app/service/graph
Before creating a new graph service, determine whether the behavior belongs in:
graph_build_service.pygraph_interaction_service.py
If neither file has an appropriate responsibility, create a narrowly scoped service under app/service/graph.
Do not create duplicate REST-only and WebSocket-only implementations of the same graph mutation.
Repository modules are responsible for:
- SQLAlchemy queries
- entity lookup
- persistence operations
- active and deleted-state filtering
- node and edge lookup
- descendant lookup
Graph persistence belongs in:
app/repository/graph_repository.py
Reuse existing repository methods where possible.
Do not create a separate repository for Part Nodes unless Part Nodes use a truly separate persistence model.
Do not construct HTTP responses or WebSocket messages in repository methods.
Pydantic request and response schemas belong under app/schema.
Graph-related REST schemas should normally be added under:
app/schema/graph
Keep these concerns separate:
- REST request schemas
- REST response schemas
- WebSocket event payload schemas
Do not reuse a WebSocket payload schema as a REST request schema merely because the fields are similar.
SQLAlchemy models and domain enums belong under:
app/model
Graph entities belong in or should reuse:
app/model/graph.pyapp/model/enum.py
Do not create a new part_nodes table when Part Nodes are represented by the existing graph node model and node type enum.
Entity-to-schema and graph conversion logic should reuse or extend:
app/converter/graph_converter.py
Do not duplicate substantial conversion logic inside routers or services.
The graph is the source of truth for nodes and edges.
Part Nodes, Property Nodes, and root-level nodes must use the existing graph persistence model and enum definitions.
- Reuse the existing Node ORM model.
- Reuse the existing node type enum.
- A Part Node must use the existing
PARTtype or its actual equivalent inapp/model/enum.py. - Do not create a separate Part Node ORM model unless one already exists.
- Validate that a node belongs to the requested room.
- Normal read APIs must not return soft-deleted nodes.
- Use UUID identifiers according to current project conventions.
- Use timezone-aware UTC timestamps if timestamps are created in application code.
- Do not allow clients to arbitrarily change immutable fields such as node ID, room ID, or node type.
- PATCH operations must modify only fields explicitly supplied by the request.
- Reuse the existing Edge ORM model.
- Preserve the parent-child relationship requested by the client.
- Validate that both endpoints belong to the same room.
- Validate that both endpoints are active.
- Preserve the intended edge label semantics.
- Do not repurpose the edge label to store
used_in_generation. - Do not create duplicate edges unless the existing domain explicitly permits them.
Before attaching a Part Node to a parent node, validate:
- the room exists and is active
- the parent node exists and is active
- the parent node belongs to the room
- the parent node type is allowed to own the requested child type
- the relationship does not violate existing graph constraints
Do not infer valid parent types without inspecting the current graph logic.
- Use soft deletion.
- Do not hard-delete graph nodes or edges unless explicitly requested.
- Reuse the existing descendant-deletion behavior.
- Reuse the existing connected-edge deletion behavior.
- A deleted parent must not leave active descendant nodes or active connected edges when current project policy requires cascade soft deletion.
- Repeated deletion requests must follow the existing idempotency or error convention.
- Normal retrieval methods must exclude deleted entities.
After a successful graph mutation, determine whether the current architecture requires rebuilding or persisting a graph snapshot.
Inspect and reuse:
app/service/graph/graph_build_service.py
Do not implement another independent snapshot builder.
The graph snapshot must be built from the graph state stored in the database.
When handling Part Node and Property Node relationships:
- preserve the current
used_in_generationcalculation - ensure that Property Node generation state is reflected in its required parent nodes
- do not modify edge labels to represent generation state
- do not rely on client-side highlighting rules as database state
- preserve the database relationships exactly as requested
Snapshot generation must not silently use stale in-memory node or edge collections after database changes.
Use flush before snapshot queries when required by the existing transaction pattern.
The project currently receives graph interaction events through:
app/api/ws_room_event.py
WebSocket connection management is implemented in:
app/service/websocket/connection_manager.py
WebSocket event schemas are under:
app/schema/websocket
app/schema/graph
app/schema/generation
app/schema/utterance
REST and WebSocket entry points must not contain separate implementations of the same graph mutation.
The preferred flow is:
REST API ────────────────┐
├── Graph interaction service
WebSocket event handler ─┘
│
├── Graph repository
├── Graph snapshot builder
└── WebSocket event publication
When implementing REST-based graph CRUD:
- reuse graph mutation services used by WebSocket handlers when possible
- extract shared service methods when substantial logic currently exists directly inside
ws_room_event.py - do not copy and paste WebSocket mutation logic into a REST router
- do not make a REST service call the WebSocket router
- do not make a WebSocket handler call an HTTP endpoint
- both entry points should call a shared service layer
Do not add a new WebSocket event type unless explicitly requested.
When an existing WebSocket event type already represents a REST mutation, reuse its existing schema and publication path only where semantically appropriate.
REST request schemas and WebSocket event schemas must remain distinct.
When implementing Part Node CRUD APIs, follow these requirements.
The create flow should normally perform:
- validate the room
- validate the requesting user or existing authorization context
- validate the parent node
- validate room ownership of the parent node
- validate the parent-child type relationship
- create a Node using the existing PART node type
- create the requested parent-child Edge
- flush graph changes
- rebuild or persist the graph snapshot if required
- publish the appropriate existing WebSocket event if required
- commit according to the existing transaction convention
- return the project-standard API response
Node creation and required Edge creation must be atomic.
A request must not succeed with a Node created but its required Edge missing.
The read flow must:
- retrieve only active nodes
- validate the room
- validate that the node belongs to the room
- validate that the node is a Part Node
- reject or hide soft-deleted nodes according to current API conventions
- return a response schema under
app/schema/graph
Use PATCH semantics unless the existing API conventions require another method.
The update flow must:
- validate the room
- retrieve an active Part Node
- validate room ownership
- update only explicitly supplied mutable fields
- reject node type changes
- reject room ID changes
- reject node ID changes
- rebuild the graph snapshot if graph-visible data changed
- publish the appropriate existing WebSocket event if required
- commit atomically
Do not overwrite omitted fields with None.
The delete flow must:
- validate the room
- retrieve the active Part Node
- validate room ownership
- apply the existing soft-delete policy
- process descendant nodes according to the current graph policy
- soft-delete connected edges according to the current graph policy
- rebuild the graph snapshot
- publish the appropriate existing WebSocket event if required
- commit atomically
Do not implement deletion by directly calling db.delete.
Graph mutations must be atomic.
A failed operation must not leave states such as:
- Part Node created without its parent Edge
- active Edge connected to a deleted node
- parent deleted while required descendants remain active
- graph data committed while snapshot rebuilding failed
- REST mutation committed while required event preparation failed
- only part of a descendant tree deleted
Follow the transaction convention already used by the project.
Before adding transaction code, inspect whether commits are currently owned by:
- API modules
- services
- repository methods
Prefer service-level transaction orchestration for multi-step graph mutations.
Avoid adding arbitrary commit() calls inside repository methods.
Use these operations intentionally:
flush()when following queries must observe pending changesrefresh()when database-generated values are neededcommit()at the established transaction boundaryrollback()through the existing exception-handling convention
Do not catch broad exceptions merely to log and continue.
Reuse the project response system under:
app/core/response
Inspect and follow:
code.pyresponse.pyexceptions.pyexception_handler.py
For WebSocket errors, inspect and follow:
ws_exceptions.pyws_exception_handler.pyws_response.py
Requirements:
- reuse
ResponseCodeor the actual equivalent - reuse the existing success response wrapper
- use project exception classes
- do not return arbitrary dictionaries when an existing response format is available
- do not expose raw database exceptions
- do not mix HTTP exceptions and WebSocket exceptions
- preserve the current error-code and HTTP-status mapping
- preserve existing logging conventions
Do not invent new response formats for Part Node APIs.
If a new response code is required, add it consistently to the existing response system rather than embedding strings in the router.
Reuse:
app/core/logger.py
app/core/performance.py
Do not replace the logging system with print.
Log enough context to diagnose failures, including applicable identifiers such as:
- room ID
- user ID
- node ID
- parent node ID
- event type
Do not log:
- API keys
- access tokens
- secrets
- complete sensitive request contents
- unnecessary embedding vectors
Preserve existing performance instrumentation when modifying instrumented API or service flows.
Alembic files are under:
app/alembic
Do not create an Alembic migration for a Part Node CRUD feature when the existing Node and Edge tables already support the feature.
Create a migration only when a real schema change is required.
Before generating a migration:
- inspect the existing graph model
- inspect existing migrations
- verify that the requested behavior cannot be represented by current columns
- explain the required schema change
Do not modify existing migration history unnecessarily.
Graph CRUD must not trigger OpenAI, Gemini, MinIO, 2D generation, or 3D generation unless explicitly required by the feature.
Relevant modules include:
app/service/generation
app/ai/prompts
app/core/minio.py
Do not introduce an external API call into basic Part Node CRUD without a clear existing requirement.
Preserve the separation between:
- graph persistence
- prompt context construction
- prompt generation
- image generation
- asset storage
- 3D model generation
For every task:
- make the smallest coherent change
- preserve current directory structure
- preserve current import paths
- preserve current naming conventions
- preserve current API contracts unless explicitly changing them
- do not reorganize unrelated files
- do not rename public classes or functions unnecessarily
- do not rewrite working modules merely for style
- do not add an unnecessary framework or dependency
- do not apply broad formatting changes
- do not modify unrelated generation, utterance, room, history, or feature flows
- do not modify Unity or XR client code during a backend-only task
A new file is acceptable when it has a clear responsibility that does not fit an existing file.
Before adding a new abstraction, explain:
- what duplication or coupling it removes
- which existing callers will use it
- why an existing service cannot reasonably own the behavior
Before writing tests, discover the existing test structure and framework.
Do not assume a test directory or fixture name that has not been inspected.
Inspect available configuration such as:
pyproject.tomlpytest.inirequirements.txtrequirements-dev.txtMakefileREADME.md- Docker configuration
- GitHub Actions workflows
- existing test files
For Part Node CRUD, cover at minimum:
- Part Node creation succeeds
- required parent Edge is created
- Node and Edge belong to the requested room
- nonexistent room is rejected
- nonexistent parent node is rejected
- soft-deleted parent node is rejected
- parent node from another room is rejected
- invalid parent type is rejected when constrained by the domain
- Part Node retrieval succeeds
- a non-Part Node cannot be retrieved through a Part Node-specific API
- soft-deleted Part Nodes are not returned
- PATCH updates only supplied fields
- immutable fields cannot be updated
- Part Node deletion performs soft deletion
- connected edges are soft-deleted
- required descendant nodes are soft-deleted
- graph snapshot is updated after creation
- graph snapshot is updated after modification
- graph snapshot is updated after deletion
- intermediate failure rolls back the complete graph mutation
Where relevant, also verify that the expected existing WebSocket event is published exactly once.
Do not use real OpenAI, Gemini, MinIO, or external network calls in CRUD unit tests.
Do not invent commands without checking the repository configuration.
Discover the correct commands for:
- application startup
- unit tests
- integration tests
- linting
- formatting
- type checking
- Alembic validation
Run the most relevant available checks after modification.
If a command cannot be run, report:
- the exact command attempted
- the failure reason
- whether the failure is caused by code, dependencies, infrastructure, or missing configuration
Do not report a test as passed unless it was actually executed successfully.
For nontrivial features, use this order.
Read the relevant model, enum, repository, service, schema, API, converter, and response files.
Before changing code, identify:
- current request entry point
- service call chain
- repository methods
- transaction boundary
- snapshot update point
- WebSocket publication point
- reusable code
- likely duplicated logic
List:
- files to modify
- files to create
- existing methods to reuse
- any logic to extract
- transaction strategy
- tests to add
Make the smallest coherent implementation that satisfies the requirements.
Run relevant tests and static checks.
Provide a concise completion report.
After completing a task, report the following.
For each file:
- path
- purpose of the change
Describe the actual call flow, for example:
HTTP request
→ API router
→ graph interaction service
→ room and graph validation
→ graph repository
→ snapshot builder
→ WebSocket event publication
→ transaction commit
→ HTTP response
Use actual class and method names from the implementation.
For each endpoint, provide:
- HTTP method
- path
- request body
- response body
- major error cases
Report:
- commands executed
- tests passed
- tests failed
- checks not executed and why
Explicitly identify any domain rules that could not be confirmed from the repository.
Unless explicitly requested, do not:
- create a
part_nodestable - create a Part Node ORM model separate from the graph Node model
- hard-delete nodes or edges
- place SQLAlchemy queries directly in API modules
- duplicate graph mutation logic between REST and WebSocket
- use edge labels as
used_in_generation - replace the existing response system
- invent new WebSocket event types
- add external AI calls to CRUD operations
- alter unrelated import paths
- move existing modules to new directories
- perform repository-wide formatting
- change public API contracts
- claim tests were run when they were not