chore(claude): bootstrap kiro steering files (Step 5)

Add .kiro/steering/ as persistent project memory for CC-SDD / Kiro
workflows. Three core files (product, tech, structure) capture
purpose, stack, and organization patterns; three custom files
(database, api-standards, error-handling) pin the load-bearing
project-specific conventions:

- group_id isolation and the Graphiti adapter / event-loop singleton
- {success, data|error} envelope and the Task polling contract
- reasoning-model output stripping and the retry_with_backoff helper

Files focus on patterns and decisions, not catalogs, per the
steering-principles "golden rule".
This commit is contained in:
Dominik Seemann 2026-05-07 09:39:38 +02:00
parent 8144290fc6
commit d4f1a9aee0
6 changed files with 786 additions and 0 deletions

View File

@ -0,0 +1,143 @@
# API Standards
These are the conventions for the **Flask backend** consumed by the
Vue frontend. Generic REST guidance is secondary to the patterns
already established in `backend/app/api/`.
## Philosophy
- The frontend is the only consumer; we optimize for *that* contract,
not for a hypothetical public API.
- Long-running work returns immediately with a `task_id`; clients
poll. There are no streaming responses or websockets.
- The backend is stateless across restarts of `Project`/`Task` data
(in-memory), with deterministic recovery on boot.
## URL Pattern
Routes live under `/api/<domain>/<action>` where domain matches the
Flask blueprint:
- `/api/graph/...``graph_bp` (`api/graph.py`)
- `/api/simulation/...``simulation_bp` (`api/simulation.py`)
- `/api/report/...``report_bp` (`api/report.py`)
Within a blueprint, resource sub-paths are accepted but **action-style
endpoints are equally common** (`/ontology/generate`,
`/project/<id>/reset`). Don't force REST verbs onto operations that
aren't naturally CRUD — match the surrounding file.
The Vite dev server proxies `/api/*` from `:3000` to `:5001`. Don't
hard-code the backend host in frontend code.
## Response Envelope
Every response uses this shape — do **not** invent a new one:
```json
// Success
{ "success": true, "data": { ... } }
// Failure
{ "success": false, "error": "Human-readable message" }
```
- `success` is always present and boolean.
- Successful responses put the payload under `data`. List endpoints may
also include sibling fields (`count`, etc.) — see
`/project/list` for the precedent.
- Error responses use `error: <string>`. There is no error-code enum.
Messages may be in English or Chinese to match the rest of the
module — keep both styles working.
- HTTP status follows the outcome: `200` on success, `400` for client
validation, `404` for missing entities, `500` for unhandled
exceptions.
## Long-Running Operations: The Task Polling Contract
This is the defining backend pattern. Anything that takes more than a
few seconds (ontology generation, graph build, profile generation,
simulation, report generation) **must** use it.
### Submit endpoint
- Validates input synchronously.
- Creates a `Task` via `TaskManager().create_task(task_type, metadata)`.
- Spawns a background `threading.Thread` that runs the work and
updates the task as it progresses.
- Returns immediately:
```json
{ "success": true, "data": { "task_id": "...", "project_id": "..." } }
```
### Background worker
- Calls `TaskManager().update_task(task_id, progress=…, message=…, progress_detail=…)`
at meaningful checkpoints (not every loop iteration).
- On success: `complete_task(task_id, result_dict)`.
- On failure: `fail_task(task_id, error_string)` — never let the
exception escape the thread; tasks must always reach a terminal
state.
### Status endpoint
- A polling endpoint (typically `/api/<domain>/task/<task_id>` or
similar) returns the current `Task.to_dict()`.
- The frontend service layer (`frontend/src/api/*.js`) handles
exponential backoff + a 5-min timeout; new endpoints don't need
custom retry logic on the client.
### Task lifecycle
`PENDING → PROCESSING → COMPLETED | FAILED`. Other status fields
(`progress` 0100, `progress_detail` dict) are advisory — the frontend
decides how to render them. Don't add new statuses without a
frontend-side change.
## Where Logic Belongs
- **`api/` (handlers)**: validate input, look up `Project`/`Task`,
dispatch to a service, format the envelope. No graph access, no LLM
calls, no `subprocess`.
- **`services/`**: all business logic, including spawning the
background thread for long-running work.
- **`models/`**: state shape only.
If a handler is doing more than a few lines of orchestration, the work
belongs in a service.
## Authentication
There is no user-level authentication today. Endpoints assume a
trusted operator on the same network (dev, Docker, internal
deployment). **Do not add ad-hoc auth checks scattered through
handlers** — if/when auth is needed, it goes through Flask middleware
and is documented in a new steering file. Until then, treat all
endpoints as authenticated by deployment.
## Versioning
No version prefix in URLs. The frontend ships with the backend in a
single repo, so backwards compatibility for the API is not a concern.
If that ever changes (public API, multiple frontend versions), version
the affected blueprint, not the whole API.
## Pagination
- `/project/list` accepts `?limit=<n>` (default 50). Match this
pattern for new list endpoints.
- Graph queries use `utils/zep_paging.py` for cursor-style paging
(legacy name; still the canonical helper).
## What Not to Do
- Don't return raw exceptions or stack traces in `error`.
- Don't bypass `TaskManager` for long-running work (e.g. with a custom
status field on `Project`).
- Don't add new response envelope shapes — extend `data`.
- Don't introduce streaming (SSE, websockets) without a steering-level
discussion; the polling model is intentional.
---
_Focus on patterns and decisions, not endpoint catalogs._

123
.kiro/steering/database.md Normal file
View File

@ -0,0 +1,123 @@
# Database / Knowledge Graph Standards
The "database" in MiroFish is **Neo4j accessed via Graphiti**, not a
relational store. There is no SQL, no migrations file, no ORM. Generic
relational guidance does not apply — these are the project-specific
patterns.
## Architecture
- **Engine**: Neo4j 5.x Community over `bolt://`.
- **Graph layer**: `graphiti-core` ≥ 0.3 — handles node/edge writes,
embeddings, hybrid search, reranking.
- **Adapter**: `backend/app/services/graphiti_adapter.py` is the **only**
module that imports `graphiti_core` directly. Every other module talks
to the graph through this adapter.
The adapter exposes a Zep-Cloud-shaped namespace
(`client.graph.add_episode(...)`, `client.graph.search(...)`, etc.) so
legacy `zep_*` services kept their existing call sites after the
migration. New code should use the same surface — do not introduce a
parallel API.
## Core Rule: `group_id` Isolation
**Every read or write to the graph must be scoped by the project's
`group_id`.** The graph is multi-tenant by construction; cross-project
access is not permitted and is grounds for rejecting a change in review.
- A project's `group_id` lives on its `Project` model and never changes
after creation.
- When constructing search filters, episode adds, or node/edge fetches,
always pass `group_id=project.group_id` (or the equivalent
`group_ids=[...]`).
- If you need data spanning projects (e.g. an admin view), aggregate
per-project at the API layer; do not query the graph without a
`group_id` filter.
## Adapter Patterns That Must Stay Intact
These are non-obvious and break subtly when violated:
- **Single Graphiti singleton.** `_get_graphiti()` lazily constructs one
`Graphiti` instance for the whole process. Do not instantiate
`Graphiti` in services or tests.
- **Persistent event loop in a dedicated thread.** All async graph calls
are dispatched through `_run(coro)` onto a single background event
loop (see `graphiti-event-loop` thread). The Neo4j async driver is
bound to whichever loop opened it; crossing loops corrupts the driver
state. Never call `asyncio.run(...)` on a Graphiti coroutine, and
never schedule one on a request thread's loop.
- **Indices and constraints on first init.** `build_indices_and_constraints()`
runs once when the singleton is created. New required indexes go
through Graphiti's mechanisms, not raw Cypher in services.
## What Belongs in the Graph
- **Entities** — Domain objects extracted by the ontology generator
(people, organizations, concepts, events, etc.).
- **Edges** — Relationships between entities, typed per the project's
generated ontology.
- **Episodes** — The raw text/units the entities were derived from;
Graphiti owns chunking and embedding.
What does **not** belong in the graph:
- Project / task metadata (lives in in-memory `ProjectManager` and
`TaskManager`).
- Simulation state (owned by OASIS subprocesses).
- User-uploaded files (filesystem only — paths, not contents, are
passed through the API).
## Schema & Ontology
- Ontology (entity types + edge types) is **generated per project** by
the LLM in step 1, stored on the `Project` model, and used to
constrain extraction during graph build.
- There is no global, hand-maintained schema file. Don't add one — the
ontology is intentionally per-project.
- Reasoning-model outputs from ontology generation are stripped of
`<think>` blocks and code fences before JSON parsing (see
`tech.md`'s "reasoning-model output stripping" decision).
## Embeddings
- `EMBEDDING_MODEL` is configurable per provider:
- OpenAI default: `text-embedding-3-small`
- Gemini: `text-embedding-004` / `gemini-embedding-001`
- Embedding model selection lives in `config.py`. Don't hard-code it in
services.
- Switching embedding model **invalidates existing project graphs**
document this if you add an option that changes the default.
## Query Patterns
- Read via the adapter's search methods (hybrid RRF recipes are wired
in `graphiti_adapter.py`); avoid raw Cypher in feature code.
- If a feature genuinely requires raw Cypher, add it as a method on the
adapter, scoped by `group_id`, with a comment explaining why
Graphiti's API is insufficient.
- Pagination over Graphiti results uses `utils/zep_paging.py` (legacy
name, still applicable).
## Startup Recovery
`_recover_stuck_projects` runs on app boot and promotes any project
left in `GRAPH_BUILDING` to `GRAPH_COMPLETED` if the graph already has
that project's nodes — handling the case where the original task was
killed by a restart. **Any new long-running graph operation must
either:**
1. Be safe to re-run from the start, OR
2. Add an analogous recovery path so a restart mid-task doesn't strand
the project.
## Backups
Graph data is treated as **regenerable from seed material**, not as
durable user data — there is no project-managed backup/restore. If a
deployment requires durability, that's an operator concern (Neo4j
backups), not a feature-code one.
---
_Focus on patterns and decisions. No environment-specific settings._

View File

@ -0,0 +1,140 @@
# Error Handling Standards
Most errors in MiroFish originate from **LLM calls**, **graph
operations**, **subprocess simulation**, or **user-uploaded files**
not classical 4xx/5xx web flows. These standards target those failure
modes specifically.
## Philosophy
- Fail fast in services; convert to a stable response envelope at the
API layer.
- Long-running tasks must always reach a terminal state
(`COMPLETED` or `FAILED`) — a stuck `PROCESSING` task is a bug.
- LLM responses are untrusted by default: validate, strip, parse, then
use.
- Background-thread errors are silent unless explicitly captured —
always wrap the work in `try/except`.
## Error Surfaces (where they appear, where they're handled)
| Surface | Handle in | Convert to |
| -------------------- | ------------------------------------------ | --------------------------------- |
| HTTP request errors | `api/` handler `try/except` + envelope | `{"success": false, "error": …}` |
| Background task | Worker thread `try/except``fail_task()` | `Task.status = FAILED` + `error` |
| LLM call failures | `retry_with_backoff` decorator | Exception bubbles after retries |
| Graph adapter errors | Caller catches & maps | Service-specific error or `Task.fail` |
| Simulation IPC | `simulation_ipc.py` catches & logs | Task fail or simulation cleanup |
| File parsing | `utils/file_parser.py` | Raised as `ValueError` to caller |
A handler should never let an exception reach Flask's default 500
formatter — wrap and return the canonical envelope instead.
## LLM-Specific Failure Modes
These are recurring and worth handling explicitly:
### 1. Reasoning-model output contamination
Some providers (MiniMax, GLM, certain Qwen variants) emit `<think>
</think>` blocks and/or markdown code fences (```` ```json ... ``` ````)
around JSON output.
**Rule:** Strip both before `json.loads(...)`. The fix lives in commit
`985f89f` for context. Any new LLM-output JSON parser must do the same
— do not call `json.loads` on raw model output.
### 2. Transient API errors
Network blips, rate limits, intermittent 5xx from the provider.
**Rule:** Use `utils/retry.py`:
```python
from app.utils.retry import retry_with_backoff
@retry_with_backoff(max_retries=3, exceptions=(SomeAPIError,))
def call_llm(...): ...
```
- Sync version: `retry_with_backoff`
- Async version: `retry_with_backoff_async`
- For batch processing where partial failure is acceptable, use
`RetryableAPIClient.call_batch_with_retry(items, fn,
continue_on_failure=True)`.
Don't write a hand-rolled retry loop — it'll drift from the project's
backoff/jitter conventions.
### 3. Schema mismatch in structured output
LLM returns valid JSON but missing/extra fields.
**Rule:** Validate with Pydantic v2 models where the call expects
structure. Fail loudly (raise) rather than silently coercing — better
to retry the LLM call than to feed bad data downstream.
## Background Task Errors
Inside a worker thread spawned from an API handler:
```python
def _worker(task_id, project_id, ...):
try:
# work
TaskManager().update_task(task_id, progress=50, message=...)
result = do_real_work(...)
TaskManager().complete_task(task_id, result)
except Exception as e:
logger.exception(f"task {task_id} failed")
TaskManager().fail_task(task_id, str(e))
```
Rules:
- The outer `except` must be broad (`Exception`) — the goal is "task
always terminates," not "narrow down failures here."
- Log the full traceback (`logger.exception`), then store a concise
`str(e)` on the task for the frontend to display.
- Never re-raise from the worker; the thread has no caller.
- Update related `Project` state (e.g. revert `GRAPH_BUILDING`
previous status) **inside** the except, before `fail_task`.
## Graph & Subprocess Errors
- **Graphiti / Neo4j errors:** caller decides — usually fail the task
with a user-friendly message; for non-fatal search failures, log and
return empty results.
- **OASIS subprocess crashes:** `simulation_ipc.py` is the single
surface. It owns lifecycle, logging, and signaling task failure.
Don't catch subprocess errors elsewhere.
- **Startup recovery:** `_recover_stuck_projects` re-classifies
projects left `GRAPH_BUILDING` after a restart — see `database.md`.
## Logging
- Use `utils/logger.get_logger('mirofish.<module>')` — never
`print` or `logging.getLogger` directly.
- Levels:
- `ERROR` — task failure, unrecoverable exception
- `WARNING` — retry triggered, transient failure, recovered state
- `INFO` — task lifecycle (created, completed), pipeline milestones
- `DEBUG` — payload shapes, intermediate counts, off by default
- User-visible log messages should go through `utils/locale.t(...)` so
they translate; internal diagnostic logs stay in the file's existing
language (English or Chinese — match the surrounding code).
- **Never log:** API keys, full LLM prompts containing user-uploaded
text (truncate or hash), Neo4j credentials, full `.env` contents.
## What Not to Do
- Don't catch `Exception` inside an API handler just to log and
continue — fail the request and return the envelope.
- Don't retry non-idempotent work (e.g. graph writes that may have
partially completed).
- Don't translate exceptions into `success: true` responses with an
embedded error message; use `success: false`.
- Don't surface raw stack traces or LLM internals to the frontend.
---
_Focus on patterns and decisions. No implementation details or exhaustive lists._

57
.kiro/steering/product.md Normal file
View File

@ -0,0 +1,57 @@
# Product Overview
MiroFish is a multi-agent **swarm intelligence prediction engine**. Given seed
material (news, policy drafts, financial signals, novel chapters, etc.) and a
natural-language prediction question, it builds a knowledge graph, populates a
parallel "digital sandbox" with thousands of personality-driven AI agents,
runs a social simulation, and returns an analytical report plus an explorable
simulated world.
The user-facing experience is a guided **5-step workflow**: Graph Build →
Environment Setup → Simulation → Report → Interaction. Long-running steps
(LLM ontology extraction, graph build, profile generation, simulation, report)
execute as background tasks the UI polls for progress.
## Core Capabilities
- **Knowledge graph construction** — Files (PDF, text) are parsed, an LLM
extracts ontology, and Graphiti writes nodes/edges into Neo4j scoped per
project (`group_id`).
- **Persona-driven agent generation** — Entities pulled from the graph become
OASIS agent profiles with traits, memory, and behavior priors.
- **Dual-platform social simulation** — CAMEL-OASIS runs Twitter and Reddit
agents in parallel rounds with a configurable action set.
- **ReACT-loop report agent** — A reasoning agent answers the prediction
question using graph tools (`SearchResult`, `InsightForge`, `Panorama`,
`Interview`).
- **Post-simulation interaction** — Users can chat with any simulated agent
or the report agent to probe results.
## Target Use Cases
- **Macro decision rehearsal** — Stress-test policies, PR strategies, or
market moves against a synthetic public before committing.
- **Public-opinion / political forecasting** — Project how an event or
narrative may diffuse across social platforms.
- **Narrative and creative simulation** — Explore alternate endings,
what-if scenarios, or fiction continuations (e.g. *Dream of the Red
Chamber* lost-ending demo).
- **Operator-led research** — Internal analysts upload reports and inspect
the resulting graph + simulation rather than running ad-hoc surveys.
## Value Proposition
MiroFish converts a static document into a **dynamic, interrogable digital
society**. Where traditional forecasting summarizes data, MiroFish lets
decision-makers *watch the future play out* — observing emergent collective
behavior, intervening from a "god view," and reading both an analytical
report and the underlying agent interactions that produced it.
The pipeline is deliberately **provider-agnostic** at the LLM layer (any
OpenAI-SDK-compatible endpoint works) and **self-hosted** at the graph layer
(Neo4j + Graphiti, no third-party graph service required), so the same
system can run from a developer laptop to a managed deployment without
vendor lock-in.
---
_Focus on patterns and purpose, not exhaustive feature lists_

166
.kiro/steering/structure.md Normal file
View File

@ -0,0 +1,166 @@
# Project Structure
## Organization Philosophy
A **monorepo split by runtime** (`backend/` Python, `frontend/` Vue),
with each side organized **by layer**:
- Backend: `api/` (HTTP) → `services/` (logic) → `models/` (state) →
`utils/` (cross-cutting helpers), with `config.py` as the single source
of configuration.
- Frontend: `views/` (route-level pages) → `components/` (reusable UI) →
`api/` (HTTP services) → `store/`, `router/`, `i18n/`, `assets/`.
The core workflow is **pipeline-shaped** (5 sequential steps), and the
codebase mirrors that: each step has a backend service or service group,
a Flask blueprint endpoint, a frontend Step component, and (where useful)
a route-level view.
## Directory Patterns
### Backend HTTP Layer
**Location**: `backend/app/api/`
**Purpose**: One Flask blueprint per pipeline domain — thin handlers
that validate input, dispatch to services, and return JSON. No business
logic.
**Files**: `graph.py` (`graph_bp`), `simulation.py` (`simulation_bp`),
`report.py` (`report_bp`).
### Backend Services
**Location**: `backend/app/services/`
**Purpose**: All business logic. Each file owns one responsibility
(graph build, profile generation, simulation runner, report agent, etc.).
**Pattern**: Long-running operations expose an async/background entrypoint
that returns a `Task` and runs work off the request thread. Direct calls
to Neo4j, OASIS subprocesses, or LLM streaming live here, not in `api/`.
**Naming note**: Files prefixed `zep_*` are legacy from the Zep→Graphiti
migration. Don't rename casually (imports across the project), and don't
add new `zep_*` files.
### Backend State Models
**Location**: `backend/app/models/`
**Purpose**: In-memory, JSON-serializable state objects. `Project`
tracks per-project pipeline state and `group_id`; `Task` tracks
background-job status, progress, and result. These are the polling
contract with the frontend — change their shape with care.
### Backend Utilities
**Location**: `backend/app/utils/`
**Purpose**: Cross-cutting helpers usable from any service —
LLM client wrapper (`llm_client.py`), file parsing (`file_parser.py`),
retry (`retry.py`), logging (`logger.py`), locale (`locale.py`),
pagination helpers (`zep_paging.py`).
**Rule**: Utils never import from `services/` or `api/`.
### Backend Config
**Location**: `backend/app/config.py`
**Purpose**: Single file for LLM, Neo4j, embedding, chunking, OASIS,
and ReportAgent parameters. Read env vars here; consume the resulting
constants elsewhere. Avoid `os.getenv` calls scattered through services.
### Frontend Views (Routes)
**Location**: `frontend/src/views/`
**Purpose**: Page-level components mapped to routes (`Home.vue`,
`MainView.vue`, `Process.vue`, `SimulationView.vue`,
`SimulationRunView.vue`, `InteractionView.vue`, `ReportView.vue`).
**Pattern**: `Process.vue` is the workflow orchestrator (~50KB); it
composes the Step components and owns step transitions.
### Frontend Components
**Location**: `frontend/src/components/`
**Purpose**: Reusable UI. Step components (`Step1GraphBuild.vue`,
`Step2EnvSetup.vue`, `Step3Simulation.vue`, `Step4Report.vue`,
`Step5Interaction.vue`) implement one pipeline stage each;
`GraphPanel.vue` renders the D3 knowledge graph;
`HistoryDatabase.vue`, `LanguageSwitcher.vue` are general-purpose.
### Frontend API Services
**Location**: `frontend/src/api/`
**Purpose**: Axios services that wrap the backend blueprints —
`graph.js`, `simulation.js`, `report.js`, with `index.js` as the shared
client (5-min timeout, exponential retry).
**Rule**: Components and views call these services; they do not import
`axios` directly. New endpoints add a method on the matching service.
### Locales (shared)
**Location**: `/locales/` at repo root
**Purpose**: i18n source for both frontend (`vue-i18n`) and backend
(logger). Vite aliases `@locales` to this folder. Files: `en.json`,
`zh.json`, `languages.json`.
### Static Assets
**Location**: `static/` at repo root
**Purpose**: Images and demo assets referenced from READMEs (logos,
screenshots, video covers). Not bundled by Vite.
## Naming Conventions
- **Python files / modules / functions / vars**: `snake_case`
- **Python classes**: `PascalCase`
- **Python constants**: `UPPER_SNAKE_CASE`
- **Vue Single-File Components**: `PascalCase.vue`
- **Vue route views**: `<Name>View.vue` or domain noun
(`Home.vue`, `Process.vue`, `MainView.vue`)
- **Vue step components**: `Step<N><Name>.vue` (matches the pipeline stage)
- **Frontend non-component JS**: `camelCase.js` (e.g.
`pendingUpload.js`)
- **Locale files**: lowercase ISO code (`en.json`, `zh.json`) +
`languages.json` for the language list
- **Booleans (Python and JS)**: prefix with `is_` / `has_` / `should_`
where it improves clarity, but match local style first
## Import Organization
### Frontend (`frontend/src/`)
```js
// Vendor
import { ref, computed } from 'vue'
import axios from 'axios'
// Absolute (via @ alias)
import GraphPanel from '@/components/GraphPanel.vue'
import { fetchGraph } from '@/api/graph'
// Locales (shared with backend)
import en from '@locales/en.json'
// Relative (same feature only)
import { useStep } from './useStep'
```
**Path aliases** (`vite.config.js`):
- `@/``frontend/src/`
- `@locales` → repo-root `/locales/`
### Backend (`backend/app/`)
- Use absolute package imports (`from app.services.graph_builder import ...`).
- Layer dependency rule: `api → services → models / utils`. Services
may import from `models` and `utils`; `models` and `utils` never
import from `services` or `api`.
- All Neo4j/Graphiti access goes through `services/graphiti_adapter.py`.
## Code Organization Principles
- **Pipeline-aligned modules.** When adding a new pipeline-touching
feature, place code in the same backend service group and the same
frontend Step component as the stage it belongs to. Don't split a
stage across multiple services unless responsibilities genuinely
diverge.
- **Background tasks are uniform.** Any operation taking more than a
few seconds returns a `Task` and is polled. Don't introduce ad-hoc
status fields on `Project`; extend `Task`.
- **Per-project isolation.** Every graph operation must filter by
`group_id`. Cross-project reads are out of scope and should be
flagged in review.
- **IPC has one door.** Subprocess communication for the simulator goes
through `services/simulation_ipc.py`. Do not call `subprocess` /
pipe primitives elsewhere.
- **Configuration is centralized.** New tunables go in
`backend/app/config.py` (and an `.env.example` line if env-driven),
not as constants scattered through services.
- **Legacy filenames stay.** `zep_*` files predate the Graphiti
migration; leave the names alone to avoid touching every importer,
but don't add new `zep_*` files.
---
_Document patterns, not file trees. New files following patterns shouldn't require updates_

157
.kiro/steering/tech.md Normal file
View File

@ -0,0 +1,157 @@
# Technology Stack
## Architecture
A two-tier web app with a long-running **background-task** core:
- **Frontend** (Vue 3 + Vite) — Single-page UI orchestrating the 5-step
workflow. Polls the backend for task progress; renders the knowledge
graph with D3.
- **Backend** (Flask + `uv`) — Stateless HTTP API on top of in-memory
`Project` and `Task` models. Heavy work (ontology extraction, graph
build, profile generation, simulation, report) runs as background
tasks tracked through `Task` and exposed via polling endpoints.
- **Knowledge graph** — Neo4j is the durable store; Graphiti is the
write/read layer. All queries are scoped by per-project `group_id`.
- **Simulation** — CAMEL-OASIS executes in subprocesses; the Flask app
communicates with them only through `services/simulation_ipc.py`.
The system favors **process isolation** for the simulator and **in-memory
state with restart recovery** for project/task tracking, rather than a
classic job queue + persistent DB.
## Core Technologies
- **Backend language**: Python ≥3.11, ≤3.12
- **Backend framework**: Flask 3.0 + flask-cors
- **Backend tooling**: `uv` for dependency management
- **Frontend framework**: Vue 3.5 + Vue Router 4 + `vue-i18n` 11
- **Frontend tooling**: Vite 7
- **Graph DB**: Neo4j 5.x (Community) via `bolt://`
- **Graph layer**: `graphiti-core` ≥ 0.3
- **Simulation**: `camel-oasis` 0.2.5 + `camel-ai` 0.2.78
- **LLM access**: OpenAI SDK against any OpenAI-compatible endpoint
## Key Libraries
Only the libraries that shape how new code is written:
- **`openai`** — Sole LLM client; new providers are integrated by changing
`LLM_BASE_URL`/`LLM_MODEL_NAME`, **not** by adding a second SDK.
- **`graphiti-core`** — All graph reads/writes go through the
`graphiti_adapter`; do not call Neo4j drivers directly from feature
code.
- **`camel-oasis` / `camel-ai`** — Pinned versions; upgrading either
requires re-validating the simulation pipeline end-to-end.
- **`PyMuPDF`, `charset-normalizer`, `chardet`** — File ingestion;
encoding detection is mandatory because seed material is frequently
non-UTF-8 (notably mixed Chinese/English).
- **`pydantic` v2** — Used for structured LLM output / validation.
- **`axios`** (frontend) — All API calls go through `src/api/*.js`
services with a 5-min timeout and exponential retry; components must
not call `fetch`/`axios` directly.
- **`d3` v7** — Knowledge-graph visualization in `GraphPanel.vue`.
## Development Standards
### Type Safety
- Python: type hints where the surrounding file uses them. Don't retrofit
hints into untyped modules just for consistency.
- Frontend: plain JavaScript, not TypeScript. Use JSDoc only when it
improves clarity.
### Code Quality
- **No enforced linter or formatter** in this repo by design. Match the
surrounding file's style. Discuss with the user before introducing
ESLint/Prettier/Ruff/Black.
- 4-space indentation everywhere.
- Python: `snake_case`. Existing files mix English and Chinese in
comments/docstrings — preserve both; do not translate one into the
other unless asked.
### Testing
- pytest is wired (`backend/scripts/test_profile_format.py`) but coverage
is intentionally minimal. Don't add a heavy test harness without
discussing scope.
- For UI changes, run `npm run dev` and exercise the feature in a
browser; type-check/test passes do not prove feature correctness here.
### Internationalization
- User-visible strings live in repo-root `/locales/*.json` (`en.json`,
`zh.json`, `languages.json`). The `frontend/vite.config.js` aliases
`@locales` to that root folder so the backend logger and frontend share
the same keys.
- Backend logger messages are part of the i18n surface — translate keys,
not raw log lines, when adding new logs that surface to users.
## Development Environment
### Required Tools
| Tool | Version |
| --------- | ------------- |
| Node.js | ≥18 |
| Python | ≥3.11, ≤3.12 |
| `uv` | latest |
| Neo4j | 5.x Community |
| Docker | optional |
### Common Commands
```bash
# Setup (one-shot)
npm run setup:all
# Dev (backend on :5001, frontend on :3000 with /api proxy)
npm run dev
# Run individually
npm run backend
npm run frontend
# Build frontend
npm run build
# Backend tests
cd backend && uv run python -m pytest
# Full stack (incl. Neo4j)
docker compose up
```
## Key Technical Decisions
- **Neo4j + Graphiti replaces Zep Cloud.** Several services still carry
the legacy `zep_*` filename prefix (`zep_tools.py`,
`zep_entity_reader.py`, `zep_graph_memory_updater.py`). New code must
not depend on Zep Cloud. The `ZEP_API_KEY` env var is kept (empty
string is fine) only for backwards compatibility.
- **Per-project graph isolation via `group_id`.** Every Graphiti read or
write must filter by the project's `group_id`. There is no
cross-project graph access.
- **Reasoning-model output stripping.** Models like MiniMax and GLM emit
`<think>` blocks and markdown fences; outputs are stripped before JSON
parsing (see commit `985f89f`). New LLM-output parsers must do the
same.
- **Background tasks via `Task` model, not a queue.** Anything taking
more than a few seconds returns immediately and tracks progress on a
`Task` object the frontend polls. There is no Celery/RQ/etc.
- **Startup recovery for stuck projects.** On boot,
`_recover_stuck_projects` promotes projects in `GRAPH_BUILDING` to
`GRAPH_COMPLETED` if Neo4j already has their nodes. New long-running
task types should follow the same recovery pattern.
- **Subprocess cleanup is centralized.** `SimulationRunner.register_cleanup()`
registers a shutdown hook so simulation subprocesses die with the app.
Don't spawn subprocesses outside this path.
- **Configuration is a single Python file.** `backend/app/config.py`
holds LLM, Neo4j, embedding, chunking, OASIS, and ReportAgent
settings. Prefer extending it over scattering env-var reads through
the codebase.
- **Default simulation parameters.** Max 10 rounds. Twitter actions:
`CREATE_POST`, `LIKE_POST`, `REPOST`, `FOLLOW`, `QUOTE_POST`,
`DO_NOTHING`. Reddit additionally: `CREATE_COMMENT`, `LIKE_COMMENT`,
`DISLIKE_*`, `SEARCH_*`, `TREND`, `REFRESH`, `MUTE`. Changes go in
`config.py`, not per-call.
---
_Document standards and patterns, not every dependency_