Skip to main content

NuCoreAI Platform

Goal

Convert natural language user queries into commands, queries, and programs for any NuCore-enabled platform (currently eisy).

Quick Start

git clone https://github.com/NuCoreAI/nucore-ai.git
cd nucore-ai
python -m venv .venv && source .venv/bin/activate
pip install -e .

Development

pip install -e . installs production dependencies only. To run the test suite, install the dev extras instead:

pip install -e ".[dev]"
# or: pip install -r requirements-dev.txt
pytest

Running the Unified Runtime

The entry point is the unified runtime: one system prompt, one native tool-calling agentic loop, no router, no per-intent directory dispatch. It executes user queries directly against a NuCore backend.

Create a runtime profile JSON first (see src/unified/runtime_config.example.json).

Minimal (no backend)

python -m unified.run_unified_runtime \
  --runtime-config src/unified/runtime_config.example.json

This drops into an interactive prompt loop (> ), reading one query per line from stdin.

With NuCore Backend (eisy)

python -m unified.run_unified_runtime \
  --runtime-config src/unified/runtime_config.example.json \
  --backend-api-classpath iox.IoXWrapper \
  --backend-api-base-url https://192.168.6.134 \
  --backend-api-username admin \
  --backend-api-password yourpassword

WebSocket Server Mode

Pass --websocket-port to run as a standalone WebSocket server instead of the interactive REPL -- no HTTP framework involved (uses the websockets package, already a project dependency). Every received message is treated as a query, and the response streams back over the same connection. Conversation history is shared across connections and keyed by session id, so a reconnect (network blip, page reload, a restarted client) finds its prior history instead of starting over empty; concurrent requests for the same session id are serialized rather than racing each other. See src/unified/README.md's "Session history across connections" section for the mechanism.

python -m unified.run_unified_runtime \
  --runtime-config src/unified/runtime_config.example.json \
  --backend-api-classpath iox.IoXWrapper \
  --backend-api-base-url https://192.168.6.134 \
  --backend-api-username admin \
  --backend-api-password yourpassword \
  --websocket-port 8765

This is a lower-level alternative to eisy_ai's FastAPI-based chat server (a separate sibling project) -- use this mode when a raw ws:// endpoint is all you need, without serving a browser UI.

Developer Tool Set (Plugin Authoring)

unified.plugin_authoring (formerly dev_tools, renamed in place -- see design/developers/impl_plan.md) is a second tool set/system prompt serving both a developer testing an already-installed plugin (Dynamic Profiles JSON validation, UOM lookup, configuring/starting/stopping/calling it) and a non-technical customer guided through a store-search/research/scaffold-generation/install flow. It runs alongside the customer-facing unified tool set in the same process, over the same connection -- switched into dynamically via the /developer//customer chat commands or a model-driven tool call, not a CLI flag (see design/developers/merged-toolsets.md).

Which tool sets exist and are reachable is entirely config-driven: each one is a named profile under nucore_runtime in the runtime config file, gated by its own enabled flag (default true). plugin_authoring additionally requires --plugin-output-root (the allowed root for generated scaffolds, CLI-only -- never read from the config file) whenever it's enabled; omitting it while enabled raises an error at startup. A live backend via --backend-api-classpath is still required, same as for unified.

The customer-facing flow's web search is Claude's own native web_search tool when the resolved LLM provider is Claude (no second API key) -- set runtime config's top-level search_engine ("brave" or "tavily", with SEARCH_ENGINE_API_KEY set) to force that fallback instead, on any provider; it's also what any non-Claude provider needs, since only Claude offers a native option here.

python -m unified.run_unified_runtime \
  --runtime-config src/unified/runtime_config.example.json \
  --plugin-output-root /usr/home/admin/.config/nucore_unified_runtime/local_plugins \
  --backend-api-classpath iox.IoXWrapper \
  --backend-api-base-url https://192.168.6.134 \
  --backend-api-username admin \
  --backend-api-password yourpassword
> I want a plugin that exposes my pool controller

The model-driven switch tool (the model calls switch_to_developer_mode/switch_to_customer_mode itself when it judges the request is out of scope for whichever tool set is currently active) works the same way in both REPL and WebSocket mode. The explicit /developer//customer chat commands (see EisyUIContext.process_message) are recognized from both a raw REPL line and a WebSocket "type": "message" payload: typing one switches this connection's active tool set and immediately prints a confirmation (or, if that tool set isn't enabled on this installation, an error) -- the same outcome the model-driven switch tool would relay.

See src/unified/plugin_authoring/README.md for the full tool list, layout, and the discovery/generation workflow.

By default the server binds TCP on 0.0.0.0 (all interfaces); pass --websocket-host to bind a specific interface instead, e.g. --websocket-host 127.0.0.1.

Unix domain socket

Give --websocket-host a unix://<path> URI instead of an IP address to serve over a Unix domain socket at <path> instead of TCP -- useful when a local reverse proxy or supervisor should reach this process without exposing a TCP port. For example, --websocket-host unix:///tmp/ai-listener serves on the socket file /tmp/ai-listener. Any value that is neither a valid IP address nor a unix://-prefixed path is rejected at startup -- there is no bare-path fallback, so a typo'd IP can't silently be treated as a socket path. The unix:// form is sufficient on its own to enter WebSocket server mode -- --websocket-port is not required (and is ignored) in this mode. A stale socket file already at that path is removed before binding, so a restart after a crash doesn't fail with "address already in use".

The socket file is always created (and forced, regardless of umask or the parent directory's group) owned by this process's own uid/primary gid with mode 0660 (rw-rw----) -- readable/writable only by that same user or group, never by anyone else.

python -m unified.run_unified_runtime \
  --runtime-config src/unified/runtime_config.example.json \
  --backend-api-classpath iox.IoXWrapper \
  --backend-api-base-url https://192.168.6.134 \
  --backend-api-username admin \
  --backend-api-password yourpassword \
  --websocket-host unix:///var/run/nucore/unified.sock

Add --websocket-client-id <uid> to require that every connecting client's real effective UID match <uid>; connections from any other UID are closed immediately (WebSocket close code 1008), before any query is processed. The UID is read from the kernel via getpeereid() (BSD/POSIX; this project targets FreeBSD/eisy, not Linux, so SO_PEERCRED doesn't apply here) -- it can't be spoofed by the connecting client. --websocket-client-id only has an effect in Unix socket mode; it's silently ignored when --websocket-host is a TCP host/IP.

python -m unified.run_unified_runtime \
  --runtime-config src/unified/runtime_config.example.json \
  --backend-api-classpath iox.IoXWrapper \
  --backend-api-base-url https://192.168.6.134 \
  --backend-api-username admin \
  --backend-api-password yourpassword \
  --websocket-host unix:///var/run/nucore/unified.sock \
  --websocket-client-id 1002

TLS (wss://)

Add --ssl-certfile/--ssl-keyfile (a PEM cert and private key, given together) to serve wss:// instead of ws:// -- needed for clients that always connect over TLS (e.g. the Eisy UI). Uses Python's standard-library ssl module, no extra dependency.

python -m unified.run_unified_runtime \
  --runtime-config src/unified/runtime_config.example.json \
  --backend-api-classpath iox.IoXWrapper \
  --backend-api-base-url https://192.168.6.134 \
  --backend-api-username admin \
  --backend-api-password yourpassword \
  --websocket-port 8765 \
  --ssl-certfile secrets/cert.pem \
  --ssl-keyfile secrets/key.pem

Generate a self-signed pair for local/dev use with:

openssl req -x509 -newkey rsa:2048 -keyout secrets/key.pem -out secrets/cert.pem \
  -days 825 -nodes -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"

secrets/ is already gitignored. A self-signed cert will trigger a browser warning unless the client is configured to skip verification.

Secrets File

Use --secrets-file to provide API keys as key/value pairs. These values are loaded into a dict and passed to provider dispatch as the environment source.

The file must be valid JSON with a single top-level object. Each property name is a secret name and each value is the string to use for lookup. Do not use shell syntax, comments, or nested structures.

Example secrets.json:

{
  "OPENAI_API_KEY": "...",
  "ANTHROPIC_API_KEY": "...",
  "GEMINI_API_KEY": "...",
  "XAI_API_KEY": "...",
  "LLAMACPP_API_KEY": "..."
}

Format rules:

  • Top level must be a JSON object.
  • Keys should be environment-style secret names such as OPENAI_API_KEY.
  • Values should be strings.
  • Duplicate or alias keys are allowed if you want to point multiple names at the same secret value.

Usage:

python -m unified.run_unified_runtime \
  --runtime-config src/unified/runtime_config.example.json \
  --secrets-file /path/to/secrets.json

Logging

The runtime supports centralized, flexible logging for both development and production use. When console logging is on (the default), DEBUG/INFO records go to stdout and WARNING/ERROR records go to stderr -- so 2>/dev/null silences errors/warnings while keeping normal output, and piping just stdout to a log aggregator won't miss error-level records mixed in. The optional log file (--log-file/NUCORE_LOG_FILE) always receives every level, regardless of the console split.

Runtime Logging Flags

python -m unified.run_unified_runtime \
  --runtime-config src/unified/runtime_config.example.json \
  --log-level DEBUG \
  --log-file logs/unified-runtime.log

Use JSON logs for ingestion by external tools:

python -m unified.run_unified_runtime \
  --runtime-config src/unified/runtime_config.example.json \
  --log-json \
  --log-file logs/unified-runtime.json.log

Disable console logs (for quiet batch or service environments):

python -m unified.run_unified_runtime \
  --runtime-config src/unified/runtime_config.example.json \
  --no-log-console \
  --log-file logs/unified-runtime.log

Logging Environment Variables

  • NUCORE_LOG_LEVEL (default: INFO)
  • NUCORE_LOG_JSON (true/false, default: false)
  • NUCORE_LOG_FILE (optional file path)
  • NUCORE_LOG_CONSOLE (true/false, default: true)

Logger Usage in Code

from utils import configure_logging, get_logger

configure_logging(level="INFO")
logger = get_logger(__name__)
logger.info("runtime started")

Prompt/Tool-Call Debug Log

Separate from the logging above: every LLM turn (system prompt + every tool call/result) is written to a JSONL debug log by PromptLogManager (src/utils/prompt_log.py) -- on by default, one JSON object per line, one line per message content block rather than one line per whole message, so a single grep '"tool_use_id":"..."' finds a tool call and its result together instead of hand-pairing lines. The system prompt is written once per unique content (hash-deduped) rather than re-embedded on every agentic-loop iteration.

python -m unified.run_unified_runtime \
  --runtime-config src/unified/runtime_config.example.json \
  --prompt-log-file logs/nucore.prompt.jsonl
  • Off by default -- --prompt-log-file is the only on/off switch, and the only place the path is set (CLI-only; never read from runtime config, same reasoning as --preferences-dir below). Its mere presence enables logging to exactly that file, creating parent directories as needed.
  • Size-managed: once the active file crosses 10MB, the oldest lines are peeled off into a dated nucore.prompt.<timestamp>.jsonl.gz archive alongside it (never just discarded), and the compress-and-rewrite runs off the event loop so it can't stall an in-flight request. Nothing is deleted on process startup -- a restart just keeps appending to whatever active file exists.

Full CLI Reference

Flag Description
--runtime-config Required path to JSON with top-level nucore_runtime
--secrets-file Optional JSON file of secret key/value pairs passed into provider client key resolution
--websocket-port Run as a native WebSocket server on this port instead of the interactive REPL. Ignored when --websocket-host is a Unix socket path
--websocket-host IP address to bind the WebSocket server to over TCP (default 0.0.0.0), or a unix://<path> URI to serve over a Unix domain socket at <path> instead -- on its own (without --websocket-port) it's enough to enter WebSocket server mode. Any other value (including a bare filesystem path with no unix:// prefix) is rejected
--websocket-client-id Unix socket mode only: required effective UID (checked via getpeereid()) of the connecting client; other UIDs are rejected. Ignored when --websocket-host is a TCP host/IP
--ssl-certfile PEM cert file; with --ssl-keyfile, serves --websocket-port over wss://
--ssl-keyfile PEM private key file; with --ssl-certfile, serves --websocket-port over wss://
--backend-api-classpath Python class path for backend API (e.g. iox.IoXWrapper)
--backend-api-base-url Base URL for backend API (http(s)://host:port, or unix:///path/to/socket for iox.IoXWrapper to connect over a Unix domain socket instead of TCP)
--backend-api-username Backend API username
--backend-api-password Backend API password
--log-level Logging level override: DEBUG, INFO, WARNING, ERROR, CRITICAL
--log-file Optional rotating log file path
--log-json Emit logs in JSON format
--no-log-console Disable console logging
--preferences-dir Directory for this installation's customer preferences (aliases/events). No default, no runtime-config fallback -- preferences are unavailable unless this is set
--prompt-log-file Exact file path for the debug prompt/tool-call log; parent directories are created as needed. No default, no runtime-config fallback -- the log is off unless this is set
--plugin-output-root Allowed root directory for generated plugin scaffolds (the plugin_authoring tool set). No default, no runtime-config fallback. Required when nucore_runtime.plugin_authoring.enabled is true; ignored otherwise

Everything else -- which tool set(s) are enabled, search_engine/search_engine_api_key, per-profile stream/max_iterations -- lives in the runtime config file only (see runtime_config.example.json and design/developers/merged-toolsets.md); there is no CLI override for any of it. --preferences-dir/--prompt-log-file/--plugin-output-root are the deliberate opposite: CLI-only, never read from the config file even if present there -- the config file holds settings a customer could reasonably supply (model, temperature, which tool sets are enabled, ...), while where preferences/logs land on disk, and which root directory generated plugin scaffolds are confined to, are deployment/host concerns the system controls.

Supported Providers

Provider Alias Env Var
Anthropic Claude claude, anthropic ANTHROPIC_API_KEY
OpenAI openai OPENAI_API_KEY
Google Gemini gemini, google GEMINI_API_KEY
xAI Grok grok, xai XAI_API_KEY
llama.cpp (local) llama.cpp, llamacpp LLAMACPP_API_KEY (optional)

Provider and model settings come from the runtime profile file passed to --runtime-config. Profiles use a provider field and do not rely on legacy llm aliases or supported_llms fallback behavior. API keys can be embedded in the profile, supplied via --secrets-file, or read from process environment variables.

Ready-to-copy example profiles: src/unified/runtime_config.example.json (Claude, the default), src/unified/runtime_config.openai.example.json, src/unified/runtime_config.grok.example.json. Copy whichever one you want over your live runtime_config.json (or pass it directly via --runtime-config) and set that provider's env var.

A profile can also set "reasoning_effort" (e.g. "none"/"low"/"medium"/"high"), forwarded to OpenAI-compatible providers only when present. Some reasoning-tier models reject function tools on /v1/chat/completions entirely unless this is explicitly set -- see the OpenAI example profile.

Using a Local (Edge) LLM with llama.cpp

Build llama.cpp

sudo apt install build-essential cmake clang libomp-dev libcurl4-openssl-dev

CPU only

cmake -B build.cpu
cmake --build build.cpu --config release

Nvidia GPU

sudo ubuntu-drivers install
sudo apt install nvidia-cuda-toolkit
cmake -B build.cuda -DGGML_CUDA=on
cmake --build build.cuda --config release

Start the Server

build.cuda/bin/llama-server \
  -m /path/to/model.gguf \
  -c 64000 --port 8013 --host 0.0.0.0 \
  -t 15 --n-gpu-layers 50 --batch-size 8192

Connect the Runtime to llama.cpp

python -m unified.run_unified_runtime \
  --runtime-config /path/to/nucore_runtime.json \
  --backend-api-classpath iox.IoXWrapper \
  --backend-api-base-url https://192.168.6.134 \
  --backend-api-username admin \
  --backend-api-password yourpassword

Runtime profile for llama.cpp (--runtime-config target):

{
  "nucore_runtime": {
    "default": {
      "provider": "llama.cpp",
      "model": "qwen3-instruct",
      "url": "http://192.168.6.113:8013/v1",
      "max_turns": 20,
      "temperature": 0.2,
      "max_tokens": 32000
    }
  }
}

Connect to Ollama (or any other OpenAI-compatible local server)

The llama.cpp provider is a generic OpenAI-compatible client, so it also works against Ollama, LM Studio, vLLM, or any other server exposing a /v1/chat/completions endpoint — just point url at that server instead:

{
  "nucore_runtime": {
    "default": {
      "provider": "llama.cpp",
      "model": "llama3.1",
      "url": "http://localhost:11434/v1",
      "max_turns": 20,
      "temperature": 0.2,
      "max_tokens": 32000
    }
  }
}

No API key is required for local servers; the adapter falls back to a placeholder key if one isn't configured.

Capabilities

Beyond device/group/routine/variable command-and-control, the unified runtime supports:

  • Diagnostics -- stateless, no session or start call, one tool call per complaint shape: diagnostics_not_responding investigates a "can't control/reach a device" complaint, diagnostics_no_status_feedback investigates a "device changed locally but NuCore didn't show it" complaint, and scene_test (INSTEON-only) sends a raw PLM group-off to a scene and reports per-member success/failure for a "some scene devices aren't responding" complaint. Each runs its whole mandatory investigation internally, in the backend, in the right order -- the model doesn't sequence individual diagnostic steps itself. restart_core_service starts/stops/restarts a core service, and run_shell_command runs a shell command on the backend host (e.g. to search the device activity log).
  • User preferences -- preference_op/list_preferences store per-user aliases (e.g. naming a device or routine) and event subscriptions, persisted across sessions.
  • Plugin management -- list_store_plugins/list_purchased_plugins/list_installed_plugins cover NuCore's plugin marketplace (browse/license/installed state); get_plugin_capabilities/ call_plugin let the model extend its own capabilities with a plugin's tools when no built-in tool covers a request. install_plugin/buy_plugin/delete_plugin don't complete anything themselves -- for security reasons, installing, purchasing, and deleting all happen on the web -- each returns a link for the customer to finish there. plugin_ops starts, stops, or restarts an installed plugin's own service -- the only way to do that; restart_core_service covers core services instead.
  • Reliability -- a fabrication guard flags (and, optionally, retries) a reply that claims a tool-mediated action completed when no tool was actually called that turn, across every tool with no per-tool maintenance. Controlled by the fabrication_guard_mode/max_fabrication_retries runtime-config keys (no CLI flag) -- see src/unified/README.md's "Fabrication guard" section.

See src/unified/prompt/system_prompt.md's per-concept sections (Devices, Groups and scenes, Folders, Variables, Routines, Plugins) for the exact tool-selection rules the model follows for each of these, and src/unified/README.md for the tool/handler layout.

Hardware

Tested with eisy.

Further Documentation

  • Unified runtime architecture and tool reference: src/unified/README.md
  • Developer tool set (plugin_authoring) reference: src/unified/plugin_authoring/README.md

Metadata

Release files for nucore-ai 3.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nucore-ai 3.0.1
File Size Uploaded
nucore_ai-3.0.1.tar.gz 440.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nucore-ai 3.0.1
File Interpreter ABI Platform
nucore_ai-3.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 966.2 kB

Release files / nucore_ai-3.0.1.tar.gz

Download URL nucore_ai-3.0.1.tar.gz
Size 440.5 kB
Tags Source
SHA-256 checksum
How to use checksums
b96862fd946d9d6c0db5a8e8ae58b4199b63a5bdf7166971a1bb3c0a36292943
BLAKE2b-256 checksum
How to use checksums
f8a8c25ecea6e61a295e5b664af4d14125869fbedf21dd4e4bbbb5307a4e5ce0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.

Transparency log

Release files / nucore_ai-3.0.1-py3-none-any.whl

Download URL nucore_ai-3.0.1-py3-none-any.whl
Size 525.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6f95c181af9f11e2917916403f9e5ecb1da11ebc5fbacdc8c17aa0da99e8237d
BLAKE2b-256 checksum
How to use checksums
c4fc5b09a0161e48d7d601bad7abb48e3fcac09ab9f5966b3f9f13f4fbed4f74
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.

Transparency log

Release history Release notifications | RSS feed

3.0.2

2 release files

This release

3.0.1 This release

2 release files

3.0.0

2 release files

2.2.1

2 release files

2.2.0

2 release files

2.1.0

2 release files

2.0.0

2 release files

1.2.1

2 release files

1.2.0

2 release files

1.1.5

2 release files

1.1.4

2 release files

1.1.3

2 release files

1.1.2

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.5

2 release files

1.0.4

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page