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-fileis the only on/off switch, and the only place the path is set (CLI-only; never read from runtime config, same reasoning as--preferences-dirbelow). 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.gzarchive 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_respondinginvestigates a "can't control/reach a device" complaint,diagnostics_no_status_feedbackinvestigates a "device changed locally but NuCore didn't show it" complaint, andscene_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_servicestarts/stops/restarts a core service, andrun_shell_commandruns a shell command on the backend host (e.g. to search the device activity log). - User preferences --
preference_op/list_preferencesstore 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_pluginscover NuCore's plugin marketplace (browse/license/installed state);get_plugin_capabilities/call_pluginlet the model extend its own capabilities with a plugin's tools when no built-in tool covers a request.install_plugin/buy_plugin/delete_plugindon'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_opsstarts, stops, or restarts an installed plugin's own service -- the only way to do that;restart_core_servicecovers 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_retriesruntime-config keys (no CLI flag) -- seesrc/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.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nucore_ai-3.0.2.tar.gz | 447.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nucore_ai-3.0.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 980.9 kB
Release files / nucore_ai-3.0.2.tar.gz
| Download URL | nucore_ai-3.0.2.tar.gz |
|---|---|
| Size | 447.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2398f00f2a826db1743b372e538023cb4a2742690bd1fc75790f1d760da306bd
|
|
BLAKE2b-256 checksum How to use checksums |
2b8e3a3c4f57e4cbad7bd4734ffffe9b8c455a94a87764a2df0b70703926422b
|
| 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 16, 2026.
Transparency logRelease files / nucore_ai-3.0.2-py3-none-any.whl
| Download URL | nucore_ai-3.0.2-py3-none-any.whl |
|---|---|
| Size | 533.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
442d24eb7657f3cd6d8affa21e6692f5af3fb075286717212faea55d3913576f
|
|
BLAKE2b-256 checksum How to use checksums |
1f8fc06656009675d0e2f190f494684e70481eca7e5112762bd3318b9a639be1
|
| 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 16, 2026.
Transparency log