Skip to content

Runtime Deployment

Source mirrored from seocho/docs/RUNTIME_DEPLOYMENT.md

Goal: one successful local run in under 5 minutes.

If you only read one runtime document first, read this one.

This page is the onboarding path. Use it for first success and local product verification, not for benchmark claims.

If you want the Python SDK path immediately, continue with /docs/python_sdk/. If you want the bring-your-own-data path immediately, continue with /docs/apply_your_data/. If you are measuring quality or latency, continue with docs/BENCHMARKS.md.

NeedReadSuccess looks like
start the local runtimeSetup -> Start the Runtimedocker compose ps shows the core services
choose the right execution pathExecution Modes Matteryou know local SDK vs runtime semantic vs debate
verify through the UIFirst Success: UI Pathone ingest and one semantic question work
verify through the APIFirst Success: Direct API Pathcurl returns route and response fields
verify through PythonFirst Success: Python SDK Pathask_response(...) returns support metadata
debug local filesKnow Where Your Files Goyou can find graph data, artifacts, and traces

Default path: use semantic graph QA first. Use react/debate only when you explicitly need agentic tool use or multi-agent comparison.

  • Docker and Docker Compose
  • OPENAI_API_KEY recommended
  • curl and jq for optional API checks

Without OPENAI_API_KEY, SEOCHO can still boot in local fallback mode for basic verification.

Important:

  • uv pip install seocho alone does not provision DozerDB/Neo4j for you.
  • local runtime success still depends on the graph backend being reachable.
  • make up starts the core local stack, not every legacy service in the repo.
  • make up rebuilds an image-backed extraction-service, so localhost:8001 reflects a known source snapshot instead of a dirty bind-mounted checkout.

SEOCHO exposes multiple query surfaces and they do not all use the same engine.

SurfaceExecution pathUse this when
Seocho.local(...).ask(...)local SDK query pathyou want a serverless ontology-first local run
Seocho(base_url=...).ask(...)runtime primary query facadeyou want one public surface that auto-routes to chat or semantic graph QA
client.semantic(...)runtime advanced semantic graph QAyou want direct control of the semantic lane for debugging or expert use
client.react(...)runtime router agentyou want provider-native reasoning plus tool use
client.advanced(...) / client.debate(...)runtime debate orchestratoryou want explicit multi-agent comparison/synthesis

Important implications:

  • local ask() is not the same benchmark target as runtime react() or advanced()
  • reasoning_mode + repair_budget belong to the semantic path
  • max_steps + tool_budget belong to the runtime agentic paths
  • if you are comparing providers fairly on tool use, do it on runtime react/debate, not local ask()
Terminal window
git clone https://github.com/tteon/seocho.git
cd seocho
make setup-env
Terminal window
make up
docker compose ps

If you explicitly want a live bind-mounted edit loop instead:

Terminal window
make up-live

Or through the local CLI (from a clone):

Terminal window
uv sync --extra dev
uv run seocho serve

Published-package local engine path:

Terminal window
uv pip install "seocho[local]"

The default core stack is:

  • neo4j
  • extraction-service
  • evaluation-interface

Expected local surfaces:

  • Platform UI: http://localhost:8501
  • Backend API docs: http://localhost:8001/docs
  • DozerDB browser: http://localhost:7474

3.1 Isolated Per-Worktree Runtime (multi-instance)

Section titled “3.1 Isolated Per-Worktree Runtime (multi-instance)”

When more than one worktree (or agent) needs to drive the runtime at the same time, boot an isolated instance instead of a second full stack. SEOCHO follows a single-neo4j, multi-database model: the heavyweight graph backend started by make up stays shared, while each instance gets its own ephemeral logical database, its own app-tier containers, and offset ports.

Terminal window
make up # shared stack once (neo4j + default app tier)
make up INSTANCE=alice # isolated app tier: offset ports + ephemeral DB
make up INSTANCE=bob # a second, concurrent, non-colliding instance

Each instance is derived deterministically from its id (src/seocho/instance.py):

idAPI portUI portephemeral DBcompose project
alice88809180wt522b276a356bseocho-alice
bob88909190wt48181acd22b3seocho-bob

The ephemeral database name is validated against the runtime contract (^[a-z][a-z0-9]{2,62}$) before it is created on the shared neo4j. Teardown removes only that instance’s app tier and drops only its logical database — the shared neo4j and every other instance are untouched:

Terminal window
make down INSTANCE=alice # docker compose down (alice) + DROP DATABASE wt522…
make down # stop the shared stack

Equivalent SDK CLI form (and --dry-run to preview without docker):

Terminal window
seocho serve --instance alice
seocho stop --instance alice
seocho serve --instance alice --dry-run --json # inspect ports/DB/command

App-tier ports are hash-derived over 200 slots, so two distinct ids can in rare cases land on the same slot (P(collision) ≈ 1% at 2 worktrees, ≈ 14% at 8); InstanceLayout.collides_with(...) detects this. A port-slot collision is an availability concern only — data isolation is unaffected because the data plane routes by the ephemeral database key, which is derived independently of the port slot (a 48-bit space; collisions negligible). This is validated in scripts/experiments/isolation_experiment.py. Pick distinct short ids per worktree (a branch name or tenant id works well).

  1. Open http://localhost:8501
  2. Use the ingest panel or click the sample flow
  3. Ask a semantic question

The default product path is:

  • ingest data
  • run semantic retrieval
  • use bounded repair only when needed
  • reserve debate for explicit advanced use

Ingest two records:

Terminal window
curl -sS -X POST http://localhost:8001/platform/ingest/raw \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "default",
"target_database": "kgruntime",
"records": [
{"id": "r1", "content": "ACME acquired Beta in 2024."},
{"id": "r2", "content": "Beta provides risk analytics to ACME."}
]
}' | jq .

Ask through the semantic endpoint:

Terminal window
curl -sS -X POST http://localhost:8001/run_agent_semantic \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "default",
"query": "What is ACME related to?",
"databases": ["kgruntime"],
"query_mode": "graph_cot",
"reasoning_mode": true,
"repair_budget": 2
}' | jq '{route, response, reasoning: .semantic_context.reasoning}'
from seocho import Seocho
client = Seocho(base_url="http://localhost:8001", workspace_id="default")
client.raw_ingest(
[
{"id": "r1", "content": "ACME acquired Beta in 2024."},
{"id": "r2", "content": "Beta provides risk analytics to ACME."},
],
target_database="kgruntime",
)
receipt = client.ask_response(
"What is ACME related to?",
databases=["kgruntime"],
cot_mode=True,
reasoning_mode=True,
repair_budget=2,
)
print(receipt.response)
print(receipt.runtime_mode)
print(receipt.semantic_context["reasoning"])
print(receipt.support.status)
print(receipt.strategy.next_mode_hint)
print(receipt.graph_cot["guardrail_verdict"]["decision"])
recent = client.semantic_runs(limit=5, route="lpg")
print(recent[0].run_id)

7. Use React Or Debate Only As Explicit Agentic Modes

Section titled “7. Use React Or Debate Only As Explicit Agentic Modes”

If you want the runtime agent loop with bounded turns and tool usage:

react = client.react(
"What changed in ACME's graph?",
graph_ids=["kgruntime"],
max_steps=6,
tool_budget=2,
)
print(react.response)

This is the correct surface for provider-native reasoning/tool-use experiments.

If you explicitly want cross-graph comparison:

advanced = client.advanced(
"Compare what each graph knows about ACME.",
graph_ids=["kgnormal", "kgfibo"],
max_steps=8,
tool_budget=3,
)
print(advanced.debate_state)

Stay on the semantic path first. Inspect semantic.support, semantic.strategy, and semantic.evidence before reaching for debate.

Terminal window
curl -sS "http://localhost:8001/semantic/runs?workspace_id=default&limit=5&route=lpg" | jq .
Terminal window
make e2e-smoke

If you author locally with schema.jsonld but ingest/query through the runtime, do not maintain a second hand-written runtime payload. Use the same ontology to build runtime-safe artifacts:

from seocho import Ontology, Seocho
ontology = Ontology.load("schema.jsonld")
client = Seocho(ontology=ontology)
artifacts = client.approved_artifacts_from_ontology()
prompt_context = client.prompt_context_from_ontology()

Ontology.load(...) also accepts .ttl, so local authoring and governance can start from Turtle directly.

If you want the explicit local engine instead of Seocho.local(...), construct Seocho(ontology=..., graph_store=..., llm=...). All three are required to activate in-process extraction and query execution.

For ontology version review before rollout:

Terminal window
seocho ontology check --schema schema.ttl
seocho ontology diff --left schema_v1.ttl --right schema_v2.ttl
seocho ontology report --schema schema_v2.ttl --output outputs/ontology_report.json

Check service state:

Terminal window
docker compose ps
docker compose logs --tail=200 extraction-service
docker compose logs --tail=200 evaluation-interface

Common issues:

  • OPENAI_API_KEY missing or placeholder only
  • port collision on 8001, 8501, 7474, or 7687
  • graph database not ready yet

The main local locations are:

  • ontology file: usually schema.jsonld
  • local graph data: data/neo4j/
  • semantic artifacts: outputs/semantic_artifacts/
  • rule profile registry: outputs/rule_profiles/rule_profiles.db
  • semantic run metadata: outputs/semantic_metadata/
  • JSONL tracing: path from SEOCHO_TRACE_JSONL_PATH

See FILES_AND_ARTIFACTS.md for the full map and inspection commands.

14. GOPTS Layer-1 PROFILE Oracle (ADR-0097, optional)

Section titled “14. GOPTS Layer-1 PROFILE Oracle (ADR-0097, optional)”

The GOPTS cost-ranked Cypher emitter (ADR-0097) ships with a Layer-1 ranking-quality evaluation harness. The PROFILE oracle is opt-in — it needs a live DozerDB to collect real db_hits per candidate plan.

Start DozerDB

Terminal window
docker compose up -d neo4j
# APOC Extended Arrow/Parquet support is installed idempotently before Neo4j.
# To prefetch or refresh the pinned local plugin artifacts explicitly:
make apoc-extended
# wait a few seconds for the bolt port to come up

Load the FIBO-lite test corpus (idempotent, workspace-scoped):

Terminal window
NEO4J_PASSWORD=... uv run python -m scripts.eval.load_gopts_fibo_corpus
# tear down: uv run python -m scripts.eval.load_gopts_fibo_corpus --teardown

Run the live Layer-1 integration test:

Terminal window
NEO4J_PASSWORD=... uv run pytest -m integration_gopts tests/seocho/integration/

The test session auto-loads the corpus on entry and tears it down on exit. It skips cleanly when NEO4J_PASSWORD is unset or the bolt endpoint is unreachable — CI without Docker stays green.

What the harness reports

For each fixture, the harness compares the GOPTS cost model’s ranking to Neo4j PROFILE’s db_hits-driven ranking:

  • avg_top1_accuracy — fraction of fixtures where cost model picks PROFILE’s top plan.
  • avg_ndcg_at_k — graded relevance fallback for near-ties.
  • avg_kendall_tau — full-ranking agreement diagnostic.

On today’s catalog the harness reports 1.0 across all three — cost model and PROFILE agree everywhere. That is a positive result: it proves the cost-model defaults are well-calibrated against actual db_hits. The harness exists to catch future calibration drift when F8 (multi-plan execution) or richer alternatives change the picks.

Skipped fixtures: the compose stack ships uniqueness constraints on FinancialMetric.name (and similar metric subclasses). The Layer-1 live runner filters out fixtures 05/06 (finance_metric_lookup, finance_metric_delta) because they need multi-year metric rows that the constraint forbids loading. Mock-oracle Layer-1 (uv run pytest tests/seocho/test_gopts_ranking.py) still covers them.

SEOCHO does not claim to embed the GOpt optimizer. It adopts the measurable operational loop described by the Neo4j/GOpt integration work:

  1. run a semantics-preserving baseline and record p50/p95 plus result hashes;
  2. capture PROFILE and normalize operators with seocho.eval.plan_audit.audit_profile;
  3. attribute delay to scans, variable expansion, eager aggregation, sorting, DB hits, or estimated/actual cardinality divergence;
  4. propose a schema-derived candidate using labelled indexed anchors, typed relationship triplets, early workspace filters, and bounded hop/row budgets;
  5. rerun on the same frozen records and use compare_plans;
  6. promote only when result hashes match and latency improves. A faster query with different results is never promotable.

Text2Cypher receives the same constraints as optimization_hints: preferred label/index access, allowed (source)-[:TYPE]->(target) triplets, maximum four hops, maximum 50 rows, and forbidden global-scan/unbounded/cartesian shapes. validate_cypher remains the enforcement boundary; prompt hints are not a security control by themselves.

The authoritative outbox projection validates its payload before graph I/O. Every node contains id, label, and properties workspace_id, memory_sequence, and schema_version. Every relationship contains source, target, type, source_label, target_label, and the same authoritative properties. Endpoint labels must match their nodes. This lets DozerDB/Neo4j use label-specific index seeks and keeps replay, provenance, and schema drift auditable. See seocho.memory.validate_projection_format.