Chat walkthrough
How ns_ai_chat & the NetSystem AI Agent Architecture Work
A complete, end-to-end technical walkthrough of the NetSystem CMS AI subsystem: from the frontend SSE chat client and PHP session proxy, through the multi-threaded Unix-socket daemon (ns_ai_daemon.py) and agy CLI subprocess, to dynamic prompt synthesis, per-stream headless Chrome CDP isolation (browser.py), authenticated CMS action dispatch (cms_call.py), and the declarative/imperative plugin lifecycle of the items module.
agy CLI + MCP/var/run/ns-ai-daemon/ns-ai-daemon.sock (0660 root:apache)BrowserContext + 300s idle reaperai_field & custom items CRUD hooksEnd-to-End Architecture & Interactive Component Map
Click any node in the architecture diagram below to inspect its exact source files, runtime process owner, communication protocol, environment variables, and responsibilities.
ns_ai_chat.js & ns_ai_chat.php
The browser UI manages multi-conversation threads, file attachments (drag-drop, clipboard paste, or file picker up to 5 files / 10 MB each), live Markdown rendering, and collapsible agent reasoning/tool-call steps.
- Stream Resumption: If a user reloads the page while the agent is still working,
chat_historyreturnsactive_stream_idand the client automatically callschat_resumeto replay all buffered SSE chunks from offset0and continue streaming live. - Action URLs: Generated in PHP with
$this->mode = 'default'before calling$this->getURLAction()so plain HTTP URLs are placed indata-*attributes.
How the ns_ai_chat Module Works (Frontend, PHP & Database)
ns_ai_chat is a standalone CMS module that bridges authenticated administrators/creators in the browser to the local AI daemon while persisting conversation history, tool traces, and attachments in MySQL.
1. Module Registration & Options (modules/registry/ns_ai_chat_reg.php)
The module registers runtime options stored in sitemap.options and exposes one AI action (share_file) back to the agent:
| Option | Type / Hook | Default | Purpose |
|---|---|---|---|
minshowlevel |
input |
10 |
Minimum user privilege level required to view/use the chat UI ($this->canSee()). |
mineditlevel |
input |
10 |
Minimum level for editing module settings or invoking share_file ($this->canEdit()). |
preset |
select |
"" |
Either "" (General Assistant) or "site_design" (Site Design & Content Assistant). |
system_prompt |
textarea |
"" |
Optional custom instructions appended to the system context for this chat instance. |
target_modules |
textarea (_var_export / _eval) |
array() |
List of module instance IDs (from sitemap.id) that this chat instance is allowed to inspect and control. |
$this->realmoduleid:
On test page section=21316, module #21319 is a clown (clone) of ns_ai_chat module #15425 (on section=15422). Because ns_ai_chat.php keys all database queries in ns_ai_conversations by (int)$this->realmoduleid (15425) and (int)$_SESSION['id'], conversations are seamlessly shared across cloned instances while still passing the current page's $this->section (21316) to ns_ai_build_context()!
2. Database Schema (ns_ai_conversations & ns_ai_messages)
Two dedicated MySQL tables store chat state, multi-turn agy session bindings, intermediate tool steps, and inline image previews:
CREATE TABLE `ns_ai_conversations` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`moduleid` int(11) NOT NULL DEFAULT 0, -- $this->realmoduleid (e.g. 15425)
`userid` int(11) NOT NULL DEFAULT 0, -- $_SESSION['id']
`agy_conversation_id` varchar(128) DEFAULT NULL,-- UUID from agy "init" event
`title` varchar(255) NOT NULL DEFAULT '', -- Auto-derived from first prompt
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
`updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
`active_stream_id` varchar(64) DEFAULT NULL, -- 32-char hex while streaming, NULL when done
PRIMARY KEY (`id`),
KEY `module_user` (`moduleid`, `userid`)
);
CREATE TABLE `ns_ai_messages` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`conversation_id` int(11) NOT NULL, -- FK to ns_ai_conversations.id
`role` varchar(32) NOT NULL DEFAULT 'user', -- 'user' | 'assistant'
`content` mediumtext NOT NULL, -- Final Markdown response text
`steps` mediumtext DEFAULT NULL, -- JSON array of thought/tool/response steps
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
`attachments` mediumtext DEFAULT NULL, -- JSON array of attachment metadata + data_uri
PRIMARY KEY (`id`),
KEY `conversation_id` (`conversation_id`)
);
Why steps is stored as JSON: As the stream progresses, PHP accumulates an ordered $steps array of {type: "thought"|"tool_call"|"response"|"error"|"aborted", ...} objects in memory and writes the complete JSON array to ns_ai_messages.steps when the stream finishes. When reloading history via chat_history, the UI reconstructs every collapsible thought and tool-call card identically to how it looked live.
3. PHP Controller Actions in modules/ns_ai_chat.php & localib/libmod.php
chat_streamStarts or continues a conversation turn and proxies the daemon's SSE stream to the browser:
- Checks
$this->canSee()(returns HTTP 403 JSON if unauthorized). - Processes
$_FILES['attachments'](up to 5 files, max 10 MB each): base64-encodes file bytes for the daemon payload, and for images ≤ 2 MB stores adata:image/...;base64,...URI in$dbAttachments. - Creates or fetches the row in
ns_ai_conversationsand inserts the'user'message row intons_ai_messages. - Generates
$streamId = bin2hex(random_bytes(16))and stores it inns_ai_conversations.active_stream_id. - Calls
ns_ai_build_context($this)to build the dynamic system prompt. - Calls
session_write_close()andignore_user_abort(true)so the PHP session lock is released before streaming begins (allowing concurrentchat_abortor agent callbacks using the samePHPSESSID) and so PHP continues saving the final message to MySQL even if the user closes their browser tab! - Sends
event: conversationand invokesns_ai_stream_daemon('/v1/chat/stream', $daemonPayload, $onEvent).
chat_resume & chat_historyProvides seamless mid-stream page reload recovery:
- When a conversation is clicked,
chat_historyreturns all savedmessagesplusactive_stream_idfromns_ai_conversations. - If
active_stream_idis non-empty,ns_ai_chat.jsimmediately connects tochat_resumewithstream_idandoffset=0. chat_resumecallsns_ai_stream_daemon('/v1/chat/resume', ['stream_id' => $streamId, 'offset' => $offset]), which replays all buffered SSE events from the daemon's memory ring-buffer and seamlessly transitions to live streaming.- Note: In
chat_resume, PHP only forwards SSE events to the browser; the originalchat_streamPHP worker (kept alive viaignore_user_abort(true)) remains solely responsible for persisting the final assistant row tons_ai_messagesso it is never saved twice!
chat_abort & chat_deleteLifecycle termination and storage cleanup:
chat_abortclearsactive_stream_id = NULLin MySQL and sendsPOST /v1/chat/abortto the daemon socket, which kills theagyprocess group (SIGTERM→SIGKILL) and disposes its headless ChromeBrowserContext.chat_deleteaborts any running stream, sendsPOST /v1/conversation/deleteto purge/var/lib/ns-ai-daemon/.gemini/antigravity-cli/brain/<agy_conversation_id>, and deletes the conversation & messages from MySQL.
share_file (AI Action)Allows the AI agent to publish files directly into the chat response:
- Invoked by the agent via
cms_call.py <domain> <chat_mod_id> share_file filename=chart.png content_base64=.... - Validates filename extension against dangerous executable extensions (
php,phtml,phar,sh,py,pl,cgi,htaccess) and caps size at 20 MB. - Writes the decoded file to
/cache/files/ai_shared/<stream_id>/<safe_name>and returns JSON withurland ready-to-pastemarkdown(for images,[name](url)for documents).
The Background Daemon Behind It (/usr/local/bin/ns_ai_daemon.py)
Because PHP-FPM workers are request-scoped and run under apache, NetSystem CMS delegates agent execution and headless browser lifecycle management to a dedicated, unprivileged Python systemd service communicating over a Unix domain socket.
1. Systemd Service & Unix Socket Security
Defined in /etc/systemd/system/ns-ai-daemon.service:
[Service]
Type=simple
User=ns-ai-daemon
Group=ns-ai-daemon
WorkingDirectory=/var/lib/ns-ai-daemon
Environment="HOME=/var/lib/ns-ai-daemon"
Environment="PATH=/var/lib/ns-ai-daemon/.local/bin:/usr/local/bin:/usr/bin:/bin"
RuntimeDirectory=ns-ai-daemon
RuntimeDirectoryMode=0750
ExecStart=/usr/bin/python3 /usr/local/bin/ns_ai_daemon.py
ExecStartPost=/bin/sh -c 'for i in 1 2 3 4 5; do [ -S /var/run/ns-ai-daemon/ns-ai-daemon.sock ] && /bin/chgrp apache /var/run/ns-ai-daemon/ns-ai-daemon.sock && exit 0; sleep 1; done; exit 1'
- Socket Permissions:
ns_ai_daemon.pybinds/var/run/ns-ai-daemon/ns-ai-daemon.sockand setsos.chmod(SOCKET_PATH, 0o660), whileExecStartPostsets its group toapache. Onlyroot,ns-ai-daemon, andapache(PHP) can connect. - Process Isolation: The agent runs as user
ns-ai-daemon, so it has zero direct filesystem write access to/vhosts/*/httpdocsor the MySQL database. All CMS reads/writes must go through authenticated HTTP endpoints viacms_call.py!
2. ActiveStream Ring Buffer & Pub/Sub Architecture
When POST /v1/chat/stream arrives, the daemon creates an ActiveStream(stream_id) instance in ACTIVE_STREAMS and spawns a background thread running _run_stream(stream, req):
class ActiveStream:
def __init__(self, stream_id):
self.stream_id = stream_id
self.events = [] # Append-only list of (event_name, data_dict)
self.cond = threading.Condition()
self.done = False
self.aborted = False
self.proc = None # subprocess.Popen handle for agy
def publish(self, event_name, data_dict):
with self.cond:
self.events.append((event_name, data_dict))
self.cond.notify_all()
def subscribe(self, offset=0):
idx = max(0, int(offset))
while True:
with self.cond:
while idx >= len(self.events) and not self.done:
self.cond.wait(timeout=15.0)
if idx >= len(self.events) and not self.done:
yield None # Emits ": heartbeat\
\
" every 15s
while idx < len(self.events):
ev = self.events[idx]; idx += 1
yield ev
if self.done and idx >= len(self.events):
break
Once stream.finish() is called, a delayed cleanup timer keeps the completed ActiveStream in ACTIVE_STREAMS for 60 seconds so late-reconnecting clients can still read the final events.
3. Daemon Filesystem Layout & Automated Storage Pruning
/var/lib/ns-ai-daemon/
├── cms_call.py # CLI helper for CMS AI actions
├── browser.py # CLI helper for CDP cookie auth & status
├── .local/bin/agy # Antigravity CLI binary
├── .gemini/
│ ├── config/mcp_config.json # Registers chrome-devtools MCP on :9222
│ └── antigravity-cli/brain/ # Multi-turn conversation trajectories
│ └── <agy_conv_id>/
│ └── .system_generated/logs/
│ ├── transcript.jsonl
│ └── transcript_full.jsonl
└── projects/
└── managedev.netsystem.gr/ # CWD for agy on this domain
├── uploads/<stream_id>/# Materialized user file attachments
└── tmp/<stream_id>/ # Agent scratch space for downloads/scripts
Background Reaper Thread (_cleanup_loop)
Runs every 15 minutes (CLEANUP_INTERVAL_SECONDS = 900) to prevent disk exhaustion:
- Session Directories (
prune_session_dirs): Deletesprojects/<domain>/tmp/<stream_id>anduploads/<stream_id>directories older than 1 hour (3600s), skipping anystream_idcurrently inACTIVE_STREAMS. - Brain Conversations Age Limit (
prune_brains): Deletes anybrain/<conv_id>directory whosemtimeis older than 30 days. - Brain Total Size Cap: If total size of
brain/exceeds 2 GiB, evicts the oldest conversations bymtimeuntil usage drops below 2 GiB.
The agy Invocation & Interactive SSE Stream Simulator
Inside _run_stream(), ns_ai_daemon.py prepares an isolated Chrome context, materializes attachments, constructs the full prompt, spawns agy with --output-format stream-json, and translates every JSONL line emitted on stdout into Server-Sent Events (SSE).
Exact agy Command Line & Subprocess Environment
# Executed by /usr/local/bin/ns_ai_daemon.py in cwd=/var/lib/ns-ai-daemon/projects/managedev.netsystem.gr
/var/lib/ns-ai-daemon/.local/bin/agy \\
-p "<system_instructions>...</system_instructions>\
\
<attached_files>...</attached_files>\
\
<user_prompt>" \\
--output-format stream-json \\
--dangerously-skip-permissions \\
--conversation "15c5b07d-87cc-4326-b1c2-4d44fd3af61f" # Omitted on turn 1; passed on subsequent turns
# Environment variables injected into the agy process group (start_new_session=True):
HOME="/var/lib/ns-ai-daemon"
PATH="/var/lib/ns-ai-daemon/.local/bin:/usr/local/bin:/usr/bin:/bin"
CMS_COOKIE="PHPSESSID=...; token=...; permpin=..." # Forwarded from PHP $_SERVER['HTTP_COOKIE']
CMS_STREAM_ID="7cd66de9c26b910e58bf40d1d440326a" # 32-char hex stream ID
CMS_BROWSER_CONTEXT_ID="E891A42F..." # Ephemeral CDP BrowserContext ID
CMS_BROWSER_TARGET_ID="39A17B0C..." # Ephemeral CDP Page Target ID
Live SSE & agy Stream-JSON Step-Through Simulator
Step through the 10 stages of a real ns_ai_chat execution turn to see how the Browser UI, PHP SSE Proxy, agy stdout JSONL, and MySQL tables stay synchronized.
agy stdout (stream-json) / Internal CallGenerated Prompts in Example Cases (ns_ai_build_context + Daemon)
Every turn sent to agy has its prompt synthesized in two stages: first by PHP's ns_ai_build_context($chatMod) in localib/libmod.php (which introspects live CMS state, permissions, presets, and target module registries), and then by Python's build_system_instructions() and process_attachments() in ns_ai_daemon.py.
How the Two-Stage Prompt Pipeline Works
localib/libmod.php:2213-2278)
Resolves the clean current page URL via getSiteBase() . str_replace('&', '&', getURL(0)) (automatically stripping /action/<id>/stream_chat and tmpvars), active languages via get_active_langs('short'), and the preset's instructions, required_modules, and optional_modules.
htmlbox Scope Metadata (localib/libmod.php:2285-2320)
Resolves each module in target_modules (handling clown clones via $modinst->getModuleName()), falls back to $modinst->getHumanID() when no manual description is given, and automatically annotates every htmlbox with [scope: page-specific | global all-pages; layers: html, css, js, head].
ns_ai_daemon.py:282-415)
Filters optional_modules against configured_module_names so only unconfigured modules are suggested, formats deduplicated ai_actions schemas, injects the isolated CDP targetId and pre-created tmp/<stream_id>/ directory, and appends process_attachments().
build_follow_up_instructions())
When resuming a conversation via --conversation <uuid>, agy already retains Turn 1 in its history. The daemon sends a compact 1.4 KB delta update (current section/URL, configured modules, new tmp/<stream_id>/, and new CDP targetId), cutting follow-up prompt overhead by 88.4%.
Inspect the Exact Generated Full Prompt Across 5 Real Scenarios
Select any scenario below to view the verbatim prompt passed to agy -p <full_prompt>. All panels support browser Find-in-Page (Ctrl+F / Cmd+F) via hidden="until-found".
[Session Context Update — Domain: managedev.netsystem.gr | Section: 21316 | URL: https://managedev.netsystem.gr/?section=21316&language=el_GR | Lang: el]
- Configured modules: treemenu #10 (Main site menu); sitelook #134 (Sitelook module); htmlbox #341 (Κύριο περιεχόμενο σελίδας με τύπο κειμένου / φωτογραφιών [scope: page-specific; layers: html, css, js, head]); htmlbox #15434 (Custom CSS που παίζει σε όλες τις σελίδες [scope: global all-pages; layers: css]); htmlbox #15435 (Custom JavaScript που παίζει σε όλες τις σελίδες [scope: global all-pages; layers: js]); items #15305 (Νέα - News)
- Session workspace directory `tmp/st_c4c800a6c70e452a8a57a54c750af212/` is pre-created (do NOT run `mkdir`). Save screenshots via `take_screenshot(filePath="tmp/st_c4c800a6c70e452a8a57a54c750af212/<name>.png")` and share via `python3 /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 21319 share_file file=tmp/st_c4c800a6c70e452a8a57a54c750af212/<filename> [title="..."]`.
- Browser active (isolated tab `targetId`: `9D663CDEEA728B25E727FE4C6EDC820D`, starts unauthenticated; run `python3 /var/lib/ns-ai-daemon/browser.py managedev.netsystem.gr auth` if auth is needed).
- Refer to the initial conversation instructions for module actions and preset rules.
User Request: Και ποιο είναι το νέο tmp directory σου σε αυτό το turn;
You are the NetSystem CMS Assistant for domain: managedev.netsystem.gr.
You manage content and settings for the modules on this website.
You have access to the local management helper `cms_call.py` and `browser.py` (execute via your `run_command` tool).
For inspecting or modifying `managedev.netsystem.gr`, ALWAYS use `cms_call.py` and Chrome DevTools browser tools — do NOT scrape `managedev.netsystem.gr` with `curl`.
STRICT SANDBOX RULE: NEVER read, grep, or write to `/vhosts/` or any server filesystem path outside your current working directory and `/var/lib/ns-ai-daemon/{cms_call.py,browser.py}`.
Current page / section ID: 21316
Current page URL: https://managedev.netsystem.gr/?section=21316&language=el_GR
Current language: el
Languages available: el (Ελληνικά), en (Αγγλικά)
--- Specialized Preset Instructions ---
### Role: NetSystem CMS Site Design & Styling Specialist
You are an expert site designer and front-end architect for websites built on NetSystem CMS.
Your goal is to help design, style, customize, and refine the visual appearance and content structure of this site.
### Architecture & Strict Module Responsibilities:
1. **Site Appearance & CSS Variables (`sitelook`) [REQUIRED]**:
- ALL global site appearance settings, brand colors, typography, spacing, layout templates, and page classes MUST be managed through the `sitelook` module.
- Always inspect and modify global settings (omit `section` or pass `section: 0`). Section-specific styling overrides should almost NEVER be used unless the user explicitly asks to style only one page differently.
- Use `get_settings` to inspect global layout, default class, ungrouped settings, and `available_groups`, then `get_settings group=<group_name>` (e.g. `group=topmenu`, `group=topbanner`, `group=content`) for group variables.
- Use `set_var` to update editable theme variables (e.g. `hlbasecolor`, font sizes). `metavars` (e.g. `hlcolor`, `hlsoftcolor`) are dynamically computed and read-only in `set_var`, but you SHOULD reference them in custom CSS (`var(--hlcolor)`, `var(--hlsoftcolor)`).
- Use `get_classes`, `set_layout`, or `set_class` when adjusting layouts or page class templates.
- **STRICT RULE**: If `sitelook` is not configured in `target_modules`, you MUST NOT write CSS overrides into `htmlbox` to change global theme variables/colors/layouts; ask the user to add the `sitelook` module ID to `target_modules`.
2. **Custom Component CSS & JavaScript (`htmlbox`) [OPTIONAL]**:
- **Per-Page vs. Global `htmlbox` Selection**: Check each `htmlbox` instance's `[scope: ...; layers: ...]` annotation in the configured modules list:
- For **page-specific** content, component CSS, or JS (affecting only the page you are creating/editing), ALWAYS pass `content`, `css`, and `js` to the **page-specific** `htmlbox` (`scope: page-specific`, with `section=<page_section_id>`) via `set_content`.
- Use a **global** `htmlbox` (`scope: global all-pages`) ONLY for site-wide HTML/CSS/JS that must appear on every page of the site.
- Inspect an `htmlbox` via `get_content` (returns `content`, `container_class` `.htmlbox<id>`, `css`, `js`, `head`) and update any combination of layers via `set_content`.
- **MANDATORY SCOPING RULE**: When writing CSS rules for an `htmlbox`, ALWAYS scope your selectors using its container class `.htmlbox<id>` (e.g. `.htmlbox<id> .my-btn { ... }` or `.htmlbox<id> { ... }`). NEVER use `#id` and NEVER use unscoped global tag selectors.
- Reference theme CSS variables (`var(--hlcolor)`, `var(--hlsoftcolor)`) instead of hardcoding brand colors.
3. **Navigation, Page Types (`class`), Structured Content & Site Metadata (`treemenu`, `items`, `siteconfig`) [OPTIONAL]**:
- Use `treemenu` (`get_tree`, `get_node`, `add_node`, `update_node`, `move_node`, `delete_node`, `duplicate_node`) to inspect or build navigation pages. When creating a new page via `add_node`, note the returned `id` (the new page's `section` ID) and pass `section=<new_id>` when populating its page-specific `htmlbox`.
- **How Page Types (`class`) & Module Rendering Work**:
- Every page (`section`) has a **Page Type (`class`)** and a **Layout (`layout`, typically `default` or `onecol`)**.
- Modules (such as a page-specific `htmlbox`, `gallery`, `items`, `forms`, or page `title`) are attached to a Page Type (`class`), and classes inherit from parent classes. When rendering a page, the CMS walks the page's `class` inheritance chain and renders only the modules belonging to that class chain (plus any modules attached directly to the page node itself).
- Consequently, **a page-specific `htmlbox` (e.g. `htmlbox #341`) will ONLY render on a page if that page's `class` includes that `htmlbox`!** If a page uses a bare skeleton class (such as `771` *Μία στήλη* or `772` *Δύο στήλες*), `htmlbox #341` is not part of that class and will NOT appear on the page until the page's `class` is set (via `treemenu` `add_node` or `update_node`) to a content Page Type such as `524`.
- Note: A `treemenu` instance may or may not be connected that gives access to all the classes. If such a `treemenu` is set up, you will be able to see every class and even create a class of your own (although creating a new class isn't really useful at the moment, as you cannot add modules on your own into that class).
- **Common Page Types (`class`) & Typical Use** (inspect full list via `sitelook` `get_classes`):
- **`524` — Σελίδα Κειμένου/Φωτογραφιών με 2 στήλες**: Standard 2-column content page (left sidebar + main content). Includes the page title, the main page-specific `htmlbox` (`#341`, via clone `#746`), and photo galleries. **Default choice** for standard content pages, "About Us", services, or migrated/custom HTML/CSS/JS pages (pair with `layout=onecol` if you want a full-width 1-column presentation without the left sidebar).
- **`1386` — Σελίδα Κειμένου/Φωτογραφιών με 2 στήλες και φόρμα επικοινωνίας**: Inherits `524` (`htmlbox #341` + galleries) and adds a contact form. Typical use: Contact Us or inquiry pages.
- **`1542` — Αρχική Σελίδα 2 στήλες**: Homepage class with homepage-specific banners and modules.
- **`1519` — Σελίδα Νέων δύο στηλών**: News / articles / announcements listing page driven by the news `items` module.
- **`1515` — Σελίδα Προϊόντων δύο στηλών**: Product catalog category page (2 columns, vertical list layout) driven by the products `items` module.
- **`1517` — Σελίδα προϊόντων δύο στηλών με οριζόντια διάταξη**: Product catalog category page in a **grid** layout ("οριζόντια διάταξη" means in a grid, 2 columns) driven by the products `items` module.
- **Base Skeleton Classes (`1` `baseclass` — every page, `49` `allpages` — all pages except the start page, `771` *Μία στήλη*, `772` *Δύο στήλες*)**: Internal structural base classes (`visible=0`) that only define shared site-wide modules or empty column containers and do **not** include `htmlbox #341` or page content modules. Do not assign these directly to content pages unless custom modules are attached directly to the page node.
- **Note on `canhavecontent` in `treemenu`**: `canhavecontent` (`0`, `1`, `-1`) ONLY controls whether a **parent menu item that has child sub-pages** links to its own clickable page in the menu (*"Παρουσία περιεχομένου στη σελίδα όταν αυτή έχει υποσελίδες"*: typically we select `0` = use menu default; `1` = yes; `-1` = no, acts only as a submenu header). It has **no effect** on leaf pages or on whether `htmlbox` renders.
- Use `items` (`describe` first, then `list_items`, `get_items`, `save_item`, `delete_items`) when working with structured catalog/article items.
- Use `siteconfig` (`get_config`, `get_var`, `set_var`, `update_config`) for site-wide titles, logos, favicons, and metadata.
4. **Design & Inspection Standards**:
- **Ignore CMS Admin UI**: When inspecting the page via Chrome DevTools as an authenticated user, `#adminbar` (and `.adminbar`, `.modedittitle`, module edit icons) is the CMS administration toolbar — it is NOT part of the public website design. NEVER target, restyle, or hide `#adminbar` unless explicitly asked.
- Ensure clean visual hierarchy, modern typography, accessible color contrast (WCAG AA), and mobile-first responsive layouts.
--------------------------------------
Module instances available:
- treemenu, id 10, Main site menu
- sitelook, id 134, Sitelook module
- htmlbox, id 341, Κύριο περιεχόμενο σελίδας με τύπο κειμένου / φωτογραφιών [scope: page-specific; layers: html, css, js, head]
- htmlbox, id 15434, Custom CSS που παίζει σε όλες τις σελίδες [scope: global all-pages; layers: css]
- htmlbox, id 15435, Custom JavaScript που παίζει σε όλες τις σελίδες [scope: global all-pages; layers: js]
- items, id 15305, Νέα - News
Proactive Setup Guidelines:
Unconfigured optional modules available for this preset: 'siteconfig' (Siteconfig module to manage site metadata, logos, favicons, and global configuration)
If you are implementing a request and determine that having an unconfigured module above would produce a cleaner, safer, or more standard solution, explain this to the user and suggest adding that module ID to target_modules.
treemenu module actions available:
- get_tree, Returns the hierarchical menu tree structure served by this treemenu instance as JSON...
- get_node, Returns full details and configuration of a specific menu node / page by ID...
- add_node, Adds a new menu node / page under the specified parent node in this menu. Parameters: ..., class (optional: Page Type (class) ID determining which modules render on the page (e.g. 524 for standard 2-column text/photos page with htmlbox)), ..., canhavecontent (optional: Only for parent nodes with child sub-pages: 0 (use menu default — typically selected), 1 (parent has its own clickable page), -1 (parent is only a submenu header). No effect on leaf pages), ...
- update_node, Updates properties of an existing menu node / page...
- delete_node, Deletes a menu node / page by ID...
- move_node, Moves/reorders a menu node up or down among siblings...
- duplicate_node, Duplicates/clones a menu node and its entire subtree...
sitelook module actions available:
- get_settings, Returns site appearance settings as JSON (global layout, default class, available_groups, ungrouped vars, and metavars when group is omitted; or only that group's variables). Parameters: group (optional: Optional group name (e.g. topmenu, topbanner, content)), section (optional: Section ID (omit or 0 for global appearance)).
- get_classes, Returns list of available page class templates with ID and hierarchical name.
- get_var, Returns the value of a specific theme / CSS variable. Parameters: name (required: Variable name), section (optional: Section ID (omit or 0 for global appearance)).
- set_var, Updates one or more editable theme / CSS variables (e.g. hlbasecolor) without altering layout or page class. Parameters: name (optional: Variable name), value (optional: New variable value), var[<name>] (optional: Variable map), section (optional: Section ID (omit or 0 for global appearance)).
...
htmlbox module actions available:
- get_content, Returns HTML content, container_class (htmlbox<id>), custom CSS stylesheet, JavaScript, and HEAD data of this htmlbox as JSON. Parameters: section (optional: Section ID (0 or omitted for global/all-pages htmlbox)), language (optional: Language code (e.g. "el", "en", or "nolang"; defaults to current language)).
- set_content, Updates HTML content, custom CSS styles, JavaScript, and/or HEAD data of this htmlbox. Content, css, js, and head can be provided together or independently. When writing component CSS, target the box wrapper using its scoped container class .htmlbox<id>. Parameters: content (optional: HTML content (string or JSON map by language; optional when updating css/js/head)), css (optional: Custom CSS stylesheet rules (scope selectors with .htmlbox<id>)), js (optional: Custom JavaScript executed on page load), head (optional: Custom raw HEAD markup (<meta>, <link>, external <script> tags)), section (optional: Section ID (0 or omitted for global/all-pages htmlbox)), language (optional: Language code (e.g. "el", "en", or "nolang"; defaults to current language)).
- htmlbox_change_notes, Updates internal admin notes for this htmlbox...
items module actions available:
- describe, Returns {module_id, title, can_edit, can_delete, fields:[{key, label, type, multilang, required, readonly, multiple?, values?, target?, filter?, group?, cssclass?, description?}], plugin_actions}. Parameters: section (optional: Category / Section ID scope), parent_item (optional: Parent item ID when nested (useparentitemidascat)).
- list_items, Returns {count, offset, has_more, field_labels?, items:[{id, name, url, ...fields}]}. Parameters: query (optional: Search keyword or #<id>), section (optional: Category / Section ID to filter by), parent_item (optional: Parent item ID when nested (useparentitemidascat)), filters (optional: JSON map of field_key => value or {"min": ..., "max": ...} range), limit (optional: Number of items to return (default: 20, max: 50, or 20 with fields)), offset (optional: Offset for pagination (default: 0)), fields (optional: Comma-separated field keys or * for all (default: summary only)), lang (optional: Language code (e.g. "el", "en")).
- get_items, Returns {field_labels?, items:[...]} with requested or all plugin fields (max 20 IDs). Parameters: ids (required: Comma-separated item IDs or JSON list (max 20)), fields (optional: Comma-separated field keys or * for all (default: *)), lang (optional: Language code (e.g. "el", "en")).
- save_item, Creates (omit id) or updates an item using exact field keys from describe (pass <field_key>=<value> args or fields=<json>). Omitted fields are preserved on update. Parameters: id (optional: Item ID to update (omit to create new item)), name (optional: Item name / title (string or JSON map by language)), section (optional: Category / Section ID for the item), parent_item (optional: Parent item ID when nested (useparentitemidascat)), fields (optional: JSON dictionary of field_key => value updates), lang (optional: Language code (e.g. "el", "en")).
- delete_items, Deletes specified items (max 20 IDs). Requires both edit permissions and the ai_allow_delete module option enabled. Parameters: ids (required: Comma-separated item IDs or JSON list (max 20)).
Configuration & Action Execution Instructions:
1. Scope (Global vs Section) & Page Types (`class`): Site-wide appearance and configuration settings (e.g. sitelook, siteconfig, global `htmlbox`) are global: omit `section` or pass `section=0` unless the user explicitly requests a section override. For page-specific `htmlbox` modules (`scope: page-specific`), pass `section=<page_section_id>`, and ensure that page's `class` (Page Type in `treemenu`) is a content class that includes that `htmlbox` (such as `524` 'Σελίδα Κειμένου/Φωτογραφιών με 2 στήλες', rather than bare skeleton classes like `771`/`772`).
2. Execute module actions via `run_command`:
python3 /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr <module_id> <action> [section=<section_id>] key=value ...
3. Multi-line File Payloads (`key=@filepath`): `cms_call.py` supports reading parameter values directly from a file by prefixing the path with `@` (e.g. `content=@tmp/st_7157f2db817b482b9d2b0af878f3e4f9/page.html`, `css=@tmp/st_7157f2db817b482b9d2b0af878f3e4f9/style.css`, `js=@tmp/st_7157f2db817b482b9d2b0af878f3e4f9/script.js`). For any multi-line HTML, CSS, or JS, save raw `.html`, `.css`, or `.js` files and pass `key=@filepath` — do NOT write Python `subprocess` wrapper scripts around `cms_call.py`, and on follow-up turns use `replace_file_content` to make targeted edits to those files instead of rewriting them from scratch.
Example (Global Settings): python3 /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr <sitelook_module_id> set_var 'name=hlbasecolor' 'value=#0066cc'
Example (Page HTMLBox via @file or inline): python3 /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr <htmlbox_module_id> set_content section=21316 content=@tmp/st_7157f2db817b482b9d2b0af878f3e4f9/page.html css=@tmp/st_7157f2db817b482b9d2b0af878f3e4f9/style.css
Example (Multilingual Content): python3 /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr <htmlbox_module_id> set_content language=en 'content=<new_content>'
After running the command, explain what you updated and confirm to the user.
Browser Capabilities & Guidelines:
- Chrome DevTools MCP tools available (call directly — do NOT inspect MCP schema files on disk):
* All page tools require an integer `pageId` parameter (use `pageId=1` for your isolated session tab, initially at `about:blank#st_7157f2db817b482b9d2b0af878f3e4f9` with `isolatedContext=...`; if multiple sessions are active, call `list_pages()` to check the `pageId` of your `isolatedContext` tab).
* `list_pages()`, `navigate_page(pageId=1, url=...)`, `take_snapshot(pageId=1)`, `take_screenshot(pageId=1, filePath=...)`, `resize_page(pageId=1, width=..., height=...)`, `emulate(pageId=1, ...)`, `evaluate_script(pageId=1, function="() => { ... }")`, `click(pageId=1, uid=...)`, `fill(pageId=1, uid=..., value=...)`, `hover(pageId=1, uid=...)`, `press_key(pageId=1, key=...)`, `wait_for(pageId=1, text=...)`, `handle_dialog(pageId=1, action="accept"|"dismiss")`.
* Combine DOM, content, and computed-style inspections into a single `evaluate_script` call that returns a compact structured object (headings, text paragraphs, image `src` and CSS `background-image` URLs, link `href`s, form inputs/options, computed styles). NEVER return full `document.body.innerHTML` or large `outerHTML` strings (>4 KB) from `evaluate_script`, as oversized outputs spill to disk files and get truncated.
* When taking full-page screenshots (`take_screenshot(pageId=1, fullPage=true)`), headless Chrome does not scroll the page first, so below-the-fold `<img loading="lazy">` elements will remain unloaded unless you omit `loading="lazy"` or scroll the page before capturing.
- Responsive & Multi-Device Testing: Test across mobile (~390px), tablet (~800px), and desktop (~1440px) viewports, plus actual CSS breakpoints.
- Browser Authentication & Guest Mode:
* Your isolated browser tab starts in unauthenticated guest mode.
* To view pages or admin interfaces as the authenticated user, run: `python3 /var/lib/ns-ai-daemon/browser.py managedev.netsystem.gr auth` (injects `HttpOnly` session cookies invisible to `document.cookie`), then navigate or reload.
* When authenticated, `#adminbar` is the CMS admin toolbar — ignore it when evaluating or styling public page layout.
* To return to public guest mode: `python3 /var/lib/ns-ai-daemon/browser.py managedev.netsystem.gr clear` (or check status via `python3 /var/lib/ns-ai-daemon/browser.py managedev.netsystem.gr status`).
Workspace File Organization & Sharing:
- Dedicated Session Directory: `tmp/st_7157f2db817b482b9d2b0af878f3e4f9/` is ALREADY created for you (do NOT run `mkdir -p tmp/st_7157f2db817b482b9d2b0af878f3e4f9`).
- Store all screenshots and temporary files inside `tmp/st_7157f2db817b482b9d2b0af878f3e4f9/` (e.g. `take_screenshot(filePath="tmp/st_7157f2db817b482b9d2b0af878f3e4f9/screenshot.png")`). NEVER create loose files in the workspace root.
- To share a screenshot or file with the user, run:
python3 /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 21319 share_file file=tmp/st_7157f2db817b482b9d2b0af878f3e4f9/<filename> [title="Description"]
- Copy the exact Markdown snippet returned by `share_file` directly into your response.
User Request: Ανέβασε αυτή την εικόνα σε ένα νέο άρθρο στα Νέα (#15305) με τίτλο "Νέα Εποχή 2026".
<system_instructions>
You are an AI assistant integrated into the NetSystem CMS for the website: managedev.netsystem.gr
Current page/section ID: 25627
Current language: el_GR
Active site languages (short codes): el
### Role: Site Design & Content Assistant
... [steps 1-7 identical to Case 1] ...
#### ⚠️ MISSING REQUIRED MODULES:
The following core modules required by the Site Design preset are NOT configured in this chat module's `target_modules` option: `sitelook`, `treemenu`.
- `sitelook`: Required to inspect and modify site theme, colors, typography, layout, and page classes.
- `treemenu`: Required to inspect the site navigation tree and create, update, move, or delete pages.
If the user asks to perform actions that require any of these missing modules, inform them immediately that the module is missing from the `ns_ai_chat` configuration (`target_modules`) and ask the site administrator to add it.
#### Note on Optional Modules:
The following optional module types are not currently configured in `target_modules`: `siteconfig`, `items`.
If the user asks to perform a task handled by one of these modules (e.g., editing global site metadata/SEO via `siteconfig`, or managing structured listings/products/articles via `items`), proactively suggest that they add the corresponding module instance to this `ns_ai_chat` module's `target_modules` option.
### Target Module Instances
You have access to the following CMS module instances and their actions:
- **Module #341** (type: `htmlbox`, name: "htmlbox")
- `get_content`: Returns HTML content, container_class (htmlbox<id>), custom CSS stylesheet, JavaScript, and HEAD data of this htmlbox as JSON (`section`: int [optional], `lang`: string [optional])
- `set_content`: Updates HTML content, custom CSS styles, JavaScript, and/or HEAD data of this htmlbox...
- **Module #21319** (type: `ns_ai_chat`, name: "clown")
- `share_file`: Publishes a base64-encoded file so it can be displayed or downloaded in the chat (`filename`: string (required), `content_base64`: string (required))
...
</system_instructions>
<system_instructions>
You are an AI assistant integrated into the NetSystem CMS for the website: managedev.netsystem.gr
Current page/section ID: 4505
Current language: el_GR
Active site languages (short codes): el
You are a helpful general-purpose assistant for this website. Help the user with their questions, content, and any configured CMS modules.
### Additional Module Instructions:
Είσαι βοηθός διαχείρισης καταλόγου προϊόντων e-shop.
### Target Module Instances
You have access to the following CMS module instances and their actions:
- **Module #636** (type: `items`, name: "items", title: "Λίστα αντικειμένων")
- `describe`: Returns available fields, types, required flags, and permissions for this items instance
- `list_items`: Lists items with optional search query, field filters, field selection, pagination (`query`: string [optional], `limit`: int [optional], `offset`: int [optional], `section`: int [optional], `parent_item`: int [optional], `fields`: string [optional], `filters`: json [optional], `lang`: string [optional])
- `get_items`: Returns full details for specific item IDs (`ids`: string (required), `fields`: string [optional], `lang`: string [optional])
- `save_item`: Creates a new item (id=0 or omitted) or updates an existing item (`id`: int [optional], `values`: json [optional], `section`: int [optional], `parent_item`: int [optional], `lang`: string [optional])
- `delete_items`: Deletes one or more items by ID (`ids`: string (required))
- **Module #15425** (type: `ns_ai_chat`, name: "ns_ai_chat")
- `share_file`: Publishes a base64-encoded file so it can be displayed or downloaded in the chat (`filename`: string (required), `content_base64`: string (required))
...
</system_instructions>
// Inside ns_ai_build_context() (localib/libmod.php lines 2362-2365):
foreach ($targetIds as $targetId) {
$inst = getModule($db, $targetId);
if (!is_object($inst)) continue;
$realInst = $inst->getInst();
if (!is_object($realInst)) $realInst = $inst;
// Permission gate at prompt generation time!
if (!$realInst->canEdit() && !$realInst->canSee()) {
continue; // Completely omitted from Target Module Instances!
}
...
}
// Even if a module is visible (canSee() == true) and therefore listed in Target Module Instances,
// every write action (set_content, add_node, update_node, save_item, set_var, etc.) checks
// $this->canEdit() at execution time and returns HTTP 403 {"status":"error","error":"Access denied"}.
How the Browser Functionality Works (ChromeManager, CDP & browser.py)
The agent can visually inspect live pages, verify responsive layouts, check console errors, and test both logged-in admin views and public visitor views using a shared Headless Chrome instance on port 9222 with strict per-stream incognito isolation.
1. ChromeManager & Per-Stream CDP Isolation
Implemented in /usr/local/bin/ns_ai_daemon.py (lines 58–207):
- On-Demand Launch: When a chat stream starts,
CHROME_MANAGER.create_isolated_context()checks if port9222responds toGET http://127.0.0.1:9222/json/version. If not, it launches/opt/chrome-for-testing/chrome --headless --no-sandbox --disable-gpu --remote-debugging-port=9222 --window-size=1920,1080. - 300-Second Idle Reaper: A daemon thread (
_reap_loop) checks every 15 seconds ifget_active_stream_count() == 0andtime.time() - last_used > 300. After 5 minutes of idle time, Chrome is terminated withSIGTERMto free RAM. - Incognito Context Creation via Raw CDP WebSocket: Using a zero-dependency RFC 6455 WebSocket client (
_ws_connect/_cdp_call), the daemon sends two browser-level CDP commands beforeagystarts:// 1. Create an isolated incognito BrowserContext (separate cookies/storage/cache) {"id": 1, "method": "Target.createBrowserContext", "params": {"disposeOnDetach": false}} // Returns: {"result": {"browserContextId": "E891A42F..."}} // 2. Create a dedicated 1920x1080 tab inside that BrowserContext {"id": 2, "method": "Target.createTarget", "params": { "url": "about:blank", "browserContextId": "E891A42F...", "width": 1920, "height": 1080 }} // Returns: {"result": {"targetId": "39A17B0C..."}} - Guaranteed Teardown: In the
finally:block of_run_stream(), the daemon callsTarget.disposeBrowserContext({"browserContextId": browser_context_id}), immediately closing the tab and wiping all cookies/state for that stream.
2. Why browser.py Exists & How It Injects HttpOnly Cookies
In NetSystem CMS, authentication relies on three cookies: PHPSESSID, token, and permpin. Because token and permpin are marked HttpOnly, JavaScript running inside the page (document.cookie) cannot see them, nor can document.cookie = ... accurately set HttpOnly flags.
/var/lib/ns-ai-daemon/browser.py solves this by connecting directly to the page's CDP WebSocket (ws://127.0.0.1:9222/devtools/page/<CMS_BROWSER_TARGET_ID>) and executing privileged CDP Storage.* commands scoped to CMS_BROWSER_CONTEXT_ID:
| Command | CDP Method Used | What It Does |
|---|---|---|
browser.py <domain> status |
Storage.getCookies |
Checks if any cookie matching <domain> is in {"token", "permpin", "PHPSESSID"}. Outputs AUTHENTICATED (active cookies: PHPSESSID, permpin, token) or UNAUTHENTICATED. |
browser.py <domain> auth |
Storage.setCookies |
Parses os.environ["CMS_COOKIE"] and injects every cookie into CMS_BROWSER_CONTEXT_ID with path="/", secure=True, and httpOnly=(name in {"token", "permpin"}). |
browser.py <domain> clear |
Storage.getCookies + Network.deleteCookies |
Fetches all cookies in the isolated context, deletes cookies belonging to <domain>, and preserves any third-party cookies. |
9222 simultaneously, each stream has its own CMS_BROWSER_TARGET_ID. The system prompt instructs the agent to always call the chrome-devtools MCP tool select_page(pageId=CMS_BROWSER_TARGET_ID) first and never call new_page or close_page.
How the Connection to the CMS is Done from the Agent (cms_call.py)
Whenever the agent needs to read or mutate CMS state, it invokes /var/lib/ns-ai-daemon/cms_call.py via its run_command tool. Here is the exact request path from the CLI invocation to the PHP module method.
Step-by-Step Request Flow: cms_call.py → Apache .htaccess → index.php → inc/layout_func.php
1. /var/lib/ns-ai-daemon/cms_call.py
# Usage:
# cms_call.py <domain> <module_id> <action> [key=value ...]
url = f"https://{domain}/action/{module_id}/{action}"
body = urllib.parse.urlencode(params).encode("utf-8")
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "NetSystem-AI-Daemon/1.0",
}
cookie = os.environ.get("CMS_COOKIE", "").strip()
if cookie:
headers["Cookie"] = cookie
req = urllib.request.Request(url, data=body, headers=headers, method="POST")
2. URL Rewrite & Kernel Action Dispatch
# 1. .htaccess rewrites /action/<id>/<action>:
RewriteRule ^action/([0-9]+)/([a-zA-Z0-9_-]+)$ \\
index.php?tmpvars[$1][action]=$2 [QSA]
# 2. inc/layout_func.php (lines 88-115) dispatches the action:
$targetinst = getModule($db, $theid);
if (is_object($targetinst)) {
if (method_exists($targetinst, $theaction)) {
// Enforces HTTP method (ai_actions) & CSRF protection:
check_crosssite_action($targetinst->getName(), $theaction);
$targetinst->$theaction(); // Calls module method, which ends in $this->aiJSON(...) -> die()
}
}
check_crosssite_action() (localib/libmod.php:295-336) Protects AI Actions:
- HTTP Method Enforcement: Checks
$modules_reg[$targetmodname]['ai_actions'][$theaction]['method']. All AI actions specify'method' => 'POST'. If a request arrives viaGET, it immediately responds withHTTP 405 Method Not Allowed:{"status":"error","error":"Method Not Allowed. Expected POST"}. - Browser CSRF Protection: If the request comes from a browser with
Sec-Fetch-Site: cross-site(or a mismatchedOriginheader) and the action is not incrosssite_actions, it aborts withHTTP 403 Forbidden. Server-to-server requests fromcms_call.py(which carry the user'sCMS_COOKIEand no cross-site browser headers) pass cleanly and inherit the exact$_SESSION['level']of the user who initiated the chat.
How CMS Modules Handle AI Calls (amodule Helpers & Core Modules)
Base class amodule in inc/base_func.php provides standardized input parsing and JSON response helpers so any CMS module can expose AI actions with minimal boilerplate.
1. Base Class AI Helpers in amodule (inc/base_func.php:789-862)
| Method | Signature & Behavior |
|---|---|
$this->aiJSON($data, $code = 200) |
Sets http_response_code($code) and Content-Type: application/json; charset=utf-8. If $data is a string and $code >= 400, wraps it as ['status' => 'error', 'error' => $data]. Outputs json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) and calls die() to prevent page redirection. |
$this->getAIPostData() |
Merges $_POST, raw JSON body from php://input (if $_POST is empty), and $this->localvars['tmpvars'] (excluding 'action') into a single associative array. |
$this->getAIArray($val) |
Accepts either a PHP array or a JSON-encoded string and returns a decoded array (or array() on failure). |
$this->parseIdList($raw) |
Accepts a JSON array ("[10, 20]"), comma-separated string ("10,20"), or array, filters for positive integers, and returns a deduplicated int[]. |
$this->aiLangValues($val, $lang = '') |
Normalizes a multilingual field input: if $val is a JSON object {"el":"...","en":"..."}, maps each language key; if it is a plain scalar string, assigns it to the target short language code (e.g. ['el' => $val]). |
$this->aiBool($val) |
Normalizes truthy strings/ints ('1', 'true', 'yes', 'on') to true and falsy values ('', '0', 'false', 'no', 'off') to false. |
2. treemenu (Navigation & Page Lifecycle)
Files: modules/treemenu.php & modules/registry/treemenu_reg.php
- Scope Isolation (
isNodeInMenu($id)): Every AI action verifies that the target node ID (orparent_id) lies within the subtree rooted at$this->getStartPage()(e.g.734onmanagedev.netsystem.gr). An agent given access to a sub-menu cannot touch pages outside that menu's subtree! - Multilingual Merging: In
update_node,_parse_multilang_input($post['name'])is merged with existing serialized language names viaarray_merge($existingName, $nodeData['name'])so updating Greek does not erase English. - Actions:
get_tree,get_node,add_node,update_node,delete_node,move_node,duplicate_node.
3. htmlbox (HTML, Scoped CSS, JS & HEAD)
Files: modules/htmlbox.php & modules/registry/htmlbox_reg.php
- Storage Model: Stores rows in the
htmlpagestable keyed by(parent_id, section, language)whereparent_idis thehtmlboxmodule ID (e.g.341),sectionis the page ID (or0whenusesection=0, as on global CSS#15434and global JS#15435), andlanguageis:'el'/'en'for HTML content (or'0'ifuselang=0)'_css'whenallowcss=1(compiled into the page's cached CSS bundle)'_js'whenallowjs=1(appended viaappendJS)'_head'whenallowhead=1(injected into<head>viaglobal_hook_html_head)
container_class:get_contentandset_contentreturn"container_class": "htmlbox341"so the agent always knows the exact wrapper class to scope its CSS rules under.
4. sitelook (Theme Variables, Layouts & Page Classes)
Files: modules/sitelook.php & modules/registry/sitelook_reg.php
- Grouped Theme Inspection (
get_settings): Without arguments, returns globallayout, defaultclass, available layouts, variablegroups(topmenu,topbanner,content, etc.), ungrouped variables (with their CSS custom property names likevar(--bodybackcolor)), andmetavars. Passinggroup=topmenureturns all variables in that group. - Granular Updates:
set_varupdates individual CSS/theme variables without touching layout or page class, and triggers theme cache regeneration.
5. siteconfig (Global SEO, Header/Footer & robots.txt)
Files: modules/siteconfig.php & modules/registry/siteconfig_reg.php
get_config/set_config: Reads and updates global site settings for the target language (sitename,metadesc,metakeywords,header,footer,head,robots_txt, and base64favicon).
How the items Module Handles AI & What It Presents to the Agent
In NetSystem CMS, items is a composite container whose fields, prices, photos, dates, categories, and relationships are provided by child plugin modules (p_*). The items AI architecture turns any arbitrary combination of plugins into a self-describing, strongly validated JSON CRUD API.
1. The 5 AI Actions of items (modules/items.php:1580-2032)
describe — Dynamic Schema & Plugin Introspection (modules/items.php:1580-1688)
When the agent calls describe, items resolves its edit master ($editmaster = $this->getEditMaster()) and inspects every child plugin:
- Built-in Fields: If
$editmaster->options['nameeditable']is enabled, adds thenamefield (type: "text",multilang: true,required: true). - Plugin Fields (
$vinst->ai_fields()): Iterates$editmaster->getPlugins(false, true), escalates$vinst->level = $this->level, and calls$vinst->ai_fields()on each plugin. Each returned field descriptor includes:key(always$vinst->getInputName(), e.g.p_textbox-15298orp_photos-15295),label,type,multilang,required,readonly, plus optionalmultiple,values,target,unit,filter, anddescription. - Category / Parent Selectors: If
sectioneditableis enabled, callsgetEditablePages()and adds asectionfield withvalues: {sectionId: label}andfilter: {"eq": "section"}. Ifparentitemeditableis enabled, addsparent_item. - Embedded Plugin Actions (
plugin_actions): If any child plugin in$this->getPlugins(false, true)is NOT anai_fieldsprovider (for example, an embeddedhtmlboxinsideitems) and has its ownai_actionsin$modules_reg,describeexposes them in aplugin_actionsarray!
list_items — Search, Plugin SQL Filtering & Field Projection (modules/items.php:1690-1753)
Executes the full items query pipeline with plugin filters and pagination:
- Filter Translation (
$vinst->ai_filter($raw_filters)): Merges top-level parameters and JSONfilters, then callsai_filter()on every plugin. For example, passingfilters='{"p_pricevalue-834":{"gte":100,"lte":500}}'causesp_price #834to return['fromvalue834' => 100, 'tovalue834' => 500]. - Native
sql_hookRe-initialization: Temporarily sets$_GET = array_merge($this->allvars, $filter_vars)and callssetupModule(-1)so every plugin'slocalvarsare refreshed beforegetItemsListRows($query, $limit, $all_cats, $offset, $total)runs its SQL hooks! - Projection (
fields): By default (whenfieldsis omitted), returns lightweight{"id": ..., "url": "..."}entries so listing 100 items doesn't flood the context window. Passingfields="*"orfields="name,p_date-15297"hydrates the requested plugin values viaaiItem()and appends afield_labelsmap.
get_items — Full Item Hydration via ai_get($nodeid, $lang) (modules/items.php:1755-1788)
Fetches complete field values for one or more item IDs (ids="99627,15333"):
- Parses
idsvia$this->parseIdList($raw_ids). - For each ID, loads the item row via
$this->aiMember($id)and verifies$this->canSee($id)(which runscanShowItemacross all plugins). - Calls
$this->aiItem($row, $fields_filter, $lang, $field_labels), which iterates every plugin on$editmasterand merges$vinst->ai_get($id, $lang)into the item JSON object alongside a top-levelfield_labelsdictionary mapping field keys (likep_textbox-15298) to human-readable labels (like"Περιγραφή").
save_item — 3-Phase Validation, Partial Update Protection & Save (modules/items.php:1790-1988)
Instead of bypassing the CMS save lifecycle with raw SQL inserts, save_item synthesizes the exact $_POST and $_FILES state expected by items::setnodeconfig(false):
// Phase 1: Populate built-in $_POST['node'] fields (name, section -> parent_id, parent_item -> item_id)
// and run $vinst->ai_prepare($id, $values, $lang) on EVERY plugin of $editmaster:
foreach ($editmaster->getPlugins(false, true) as $aplugin) {
$vinst = $editmaster->getPluginInst($aplugin)->getInst();
if (!method_exists($vinst, 'ai_prepare')) continue;
$err = $vinst->ai_prepare($id, $values, $lang);
if ($err !== '') {
// Immediate atomic abort BEFORE setnodeconfig(false) writes anything to DB!
$this->aiJSON($err, 400);
}
}
// Phase 2: Partial Update Protection for Existing Items ($id > 0)
if ($id > 0) {
// Setting iscsvupload = 1 tells items::setnodeconfig() and plugins (like p_inputbox, p_textbox, p_group)
// to ONLY update plugins whose input keys are present in $_POST, preserving all unsubmitted fields!
$editmaster->iscsvupload = 1;
}
// Phase 3: Execute standard CMS save pipeline (canSaveNode -> preSaveNode -> saveNode -> postSaveNode)
$saved_id = $editmaster->setnodeconfig(false);
delete_items — Permission-Checked Deletion & Plugin Cleanup (modules/items.php:1990-2032)
Deletes one or more items and triggers all plugin cleanup hooks (delete_hook_pre and delete_hook):
- Verifies
$this->canDelete()(which checks$editmaster->options['deletable']and$this->canEdit()). Notice on module#636,"can_delete": falseindescribebecausedeletableis disabled in its options! - For each ID in
ids, verifies$this->canEdit($id)and$this->aiMember($id), then calls$editmaster->delete($id)and returns{"status": "ok", "deleted": [...]}.
2. Zero-Boilerplate Declarative 'ai_field' vs. Custom Plugin Overrides
In inc/base_func.php (lines 864–1088), amodule implements default ai_fields(), ai_get($nodeid, $lang), ai_prepare($nodeid, $values, $lang), and ai_filter($filters) methods driven entirely by a declarative 'ai_field' array in modules/registry/p_*_reg.php!
Declarative Example: modules/registry/p_textbox_reg.php
'ai_field' => array(
'type' => array(
'option' => 'type',
'map' => array('html' => 'html', 'text' => 'textarea'),
),
'label' => array('option' => 'label', 'default' => _('Κείμενο')),
'multilang_option' => 'uselang',
'required_option' => 'required',
'post_format' => 'lang_array',
),
With those 10 declarative lines in p_textbox_reg.php, modules/p_textbox.php requires zero PHP methods for AI support! amodule automatically reads $this->data_get($nodeid) in ai_get() and populates $_POST["p_textbox-15298"][$lang] in ai_prepare().
How post_format Maps Values into $_POST
'lang_array'(p_textbox,p_inputbox): Populates$_POST[$key][$lang] = $val(or$_POST[$key]['nolang'] = $valwhenuselang == 0).'scalar'(p_OrderPriority,p_foruser,p_quantity): Populates$_POST[$key] = (string)$val.'boolean_1_0'(p_published): Populates$_POST[$key] = $this->aiBool($val) ? '1' : '0'.'boolean_checkbox'(p_group): Sets$_POST[$key] = 1when true, orunset($_POST[$key])when false (matching HTML checkbox behavior).
Complete Reference Matrix of All items Plugins & Their AI Behavior
| Plugin | Mechanism | Key Format (getInputName()) |
AI Type | ai_get() Output |
ai_prepare() & Validation Behavior |
ai_filter() Mapping |
|---|---|---|---|---|---|---|
p_inputbox |
Declarative + Custom File Upload | p_inputbox-<id> |
string | number | boolean | select | file |
Scalar string/number, array (if multiple), or file URL |
Validates select options against values; for type=file downloads HTTP(S) URL or local path into $_FILES. |
gte → fromvalue<id>lte → tovalue<id>eq → thisvalue<id> |
p_textbox |
Declarative (ai_field) |
p_textbox-<id> |
html | textarea |
String (or {lang: text} when lang=all) |
Populates $_POST["p_textbox-<id>"][$lang] (or ['nolang']). Enforces required option. |
Full-text search via query param |
p_photos |
Custom (modules/p_photos.php) |
p_photos-<id> |
photos (multiple: true) |
[{"id": 210, "url": "/cache/files/....jpg"}, ...] |
Accepts ordered list of existing photo IDs (kept) and HTTP(S) URLs (downloaded via SSRF-safe _aiFetchImage() into $_FILES). Any existing photo ID omitted from the list is marked $_POST["delete<id>"][<pid>] = 1. Reorders in postSaveNode()! |
None |
p_price |
Custom (modules/p_price.php) |
p_pricevalue-<id> or p_pricevalue-<id>-<relid> |
price | money |
float (e.g. 2200) |
When usegroup is off, sets $_POST["p_pricevalue-<id>"]. When usegroup is on (e.g. shipping per carrier #1381), exposes one field per carrier item and sets $_POST["p_pricevalue-<id>"][<relid>]. |
gte → fromvalue<id>lte → tovalue<id>eq → thisvalue<id> |
p_group |
Declarative (ai_field) |
p_group-<id> |
boolean |
true | false |
Only exposed when editable == 1 (hidden when used purely as the structural group filter of items). Sets/unsets checkbox $_POST["p_group-<id>"]. |
Automatic via sql_hook |
p_group_add |
Custom (modules/p_group_add.php) |
p_group_add-<id> |
relation |
int[] of related item IDs |
Exposes target (linked p_group module ID) and inline values: {id: name} when ≤ 50 target items exist. Validates every submitted ID against getGroupItems() and populates $_POST["p_group_add-<id>"]. |
eq → filter<target> |
p_date |
Declarative + Custom Prepare/Filter | p_date-<id> |
date | datetime |
"YYYY-MM-DD" (from sitemap.edate) |
Validates ISO date string via strtotime() and populates $_POST['node']['edate']. |
gte → from_p_date<id>lte → to_p_date<id> |
p_published |
Declarative + Custom Filter | p_published-<id> |
boolean |
true | false (from sitemap.published) |
Populates $_POST["p_published-<id>"] = '1' | '0'. |
eq → published<id> ('1' or '2' for unpublished) |
p_OrderPriority, p_foruser, p_quantity |
Declarative (ai_field) |
<modname>-<id> |
number / select |
Number or selected user ID | Populates scalar $_POST[$key]. |
Standard filter map if configured |
p_availability, p_user, p_rating, p_comments |
Declarative (readonly) |
<modname>-<id> |
text / number |
Read-only computed value | Marked readonly: true (`when => false` for edit); cannot be overwritten via save_item. |
None |
3. What items Actually Presents to the Agent: Live Outputs from #15305 (News) & #636 (Products)
Click between the tabs below to inspect the actual verbatim JSON responses returned by cms_call.py on managedev.netsystem.gr:
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 15305 describe
Action 'describe' on module #15305 executed successfully.
Response: {
"module_id": 15305,
"title": "Νέα - News",
"can_edit": true,
"can_delete": true,
"fields": [
{
"key": "name",
"label": "Όνομα",
"type": "text",
"multilang": true,
"required": true,
"readonly": false
},
{
"key": "p_date-15297",
"label": "Ημερομηνία",
"type": "date",
"multilang": false,
"required": false,
"readonly": false,
"filter": { "gte": "from_p_date15297", "lte": "to_p_date15297" }
},
{
"key": "p_textbox-15298",
"label": "Περιγραφή",
"type": "html",
"multilang": true,
"required": false,
"readonly": false
},
{
"key": "p_photos-15414",
"label": "Φωτογραφία",
"type": "photos",
"multilang": false,
"required": false,
"readonly": false,
"multiple": true,
"description": "Ταξινομημένη λίστα με ids υπαρχουσών φωτογραφιών (διατηρούνται) και/ή http(s) URLs εικόνων (προστίθενται). Οι φωτογραφίες που δεν υπάρχουν στη λίστα διαγράφονται, οπότε για να προστεθεί μία διατηρώντας τις υπάρχουσες, συμπεριλάβετε και τα ids τους. Κενή λίστα διαγράφει όλες"
},
{ "key": "p_photos-15415", "label": "Φωτογραφία", "type": "photos", "multiple": true, ... },
{ "key": "p_photos-15416", "label": "Φωτογραφία", "type": "photos", "multiple": true, ... },
{ "key": "p_photos-15417", "label": "Φωτογραφία", "type": "photos", "multiple": true, ... },
{ "key": "p_textbox-15418", "label": "Περιγραφή", "type": "html", "multilang": true, ... },
{ "key": "p_photos-15419", "label": "Φωτογραφία", "type": "photos", "multiple": true, ... },
{ "key": "p_textbox-15420", "label": "Περιγραφή", "type": "html", "multilang": true, ... },
{ "key": "p_textbox-15421", "label": "Περιγραφή", "type": "html", "multilang": true, ... },
{ "key": "p_textbox-15300", "label": "Περιγραφή", "type": "html", "multilang": true, ... },
{ "key": "p_photos-15295", "label": "Φωτογραφία", "type": "photos", "multiple": true, ... },
{ "key": "p_photos-15301", "label": "Φωτογραφία", "type": "photos", "multiple": true, ... }
]
}
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 15305 get_items ids=99627
Action 'get_items' on module #15305 executed successfully.
Response: {
"items": [
{
"id": 99627,
"url": "/?section=7&language=el_GR&itemid15305=99627&detail15305=1",
"p_date-15297": "2026-10-14",
"p_textbox-15298": "<p class=\\"lead\\"><strong>Μια λαμπερή βραδιά γεμάτη αγάπη, φίλους και μια συγκινητική ιστορία που έστειλε το γλέντι μέχρι τον ουρανό.</strong></p>...",
"p_photos-15414": [],
"p_photos-15415": [],
"p_photos-15416": [],
"p_photos-15417": [],
"p_textbox-15418": "<h3>Μια βραδιά που έφτασε μέχρι τον ουρανό</h3>...",
"p_photos-15419": [],
"p_textbox-15420": "<h3>Ένας γάμος με αλήθεια</h3>...",
"p_textbox-15421": "",
"p_textbox-15300": "<p>Ο γάμος της Ελισάβετ Μπάρλου και του Γιώργου Σγούρα στην Πάτρα...</p>",
"p_photos-15295": [
{ "id": 210, "url": "/cache/files/1721d3bc3c20efb520cc8aaeadaf53b6990ae6cd.jpg" },
{ "id": 211, "url": "/cache/files/b313e7c3cda3a1627f23694b872ce5f400041558.jpg" }
],
"p_photos-15301": [
{ "id": 212, "url": "/cache/files/f889061998230cac32539ddfabb0c077ed7727cc.jpg" },
{ "id": 213, "url": "/cache/files/a96a339c17780de045a3b8880456389d2c5c9e1f.jpg" },
{ "id": 214, "url": "/cache/files/be83863225b531413cb0d3937eff0c9c40606947.jpg" }
]
}
],
"field_labels": {
"p_date-15297": "Ημερομηνία",
"p_textbox-15298": "Περιγραφή",
"p_photos-15295": "Φωτογραφία",
"p_photos-15301": "Φωτογραφία"
}
}
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 636 describe
Action 'describe' on module #636 executed successfully.
Response: {
"module_id": 636,
"title": "Λίστα αντικειμένων",
"can_edit": true,
"can_delete": false,
"fields": [
{ "key": "name", "label": "Όνομα", "type": "text", "multilang": true, "required": true, "readonly": false },
{ "key": "p_photos-683", "label": "Φωτογραφία", "type": "photos", "multiple": true, "readonly": false, ... },
{ "key": "p_group-762", "label": "Προσθήκη στην Πρώτη Σελίδα", "type": "boolean", "cssclass": "group", "description": "Αν το αντικείμενο ανήκει στην ομάδα" },
{
"key": "p_pricevalue-834",
"label": "Τιμή",
"type": "price",
"unit": "€",
"filter": { "gte": "fromvalue834", "lte": "tovalue834", "eq": "thisvalue834" }
},
{
"key": "p_pricevalue-1381-1374",
"label": "Έξτρα κόστος αποστολής - ΕΛΤΑ (Ελλάδα)",
"type": "price",
"unit": "Euro (-1: Καθόλου κόστος, -2: Αδυναμία αποστολής)",
"related_item": { "id": 1374, "name": "ΕΛΤΑ (Ελλάδα)" }
},
{
"key": "p_pricevalue-1381-1375",
"label": "Έξτρα κόστος αποστολής - Courier (Ελλάδα)",
"type": "price",
"unit": "Euro (-1: Καθόλου κόστος, -2: Αδυναμία αποστολής)",
"related_item": { "id": 1375, "name": "Courier (Ελλάδα)" }
},
{
"key": "p_group_add-1448",
"label": "Κατασκευαστής",
"type": "relation",
"target": 1439,
"values": [],
"description": "Λίστα με ids αντικειμένων (JSON ή χωρισμένα με κόμμα). Κενό για καθαρισμό"
},
{
"key": "p_inputbox-1190",
"label": "Κωδικός Προϊόντος",
"type": "string",
"filter": { "gte": "fromvalue1190", "lte": "tovalue1190", "eq": "thisvalue1190" }
},
{ "key": "p_availability-1198", "label": "Διαθεσιμότητα", "type": "text", "readonly": true },
{ "key": "p_textbox-685", "label": "Περιγραφή", "type": "html", "multilang": true },
{ "key": "p_group-1201", "label": "Προσθήκη στο Wishlist", "type": "boolean", "cssclass": "group" },
{
"key": "section",
"label": "Κατηγορία",
"type": "select",
"required": true,
"filter": { "eq": "section" },
"values": { "7": "Αρχική Σελίδα" }
}
],
"plugin_actions": [
{
"moduleid": 1338,
"plugin": "htmlbox",
"action": "get_content",
"description": "Returns HTML content, container_class (htmlbox<id>), custom CSS stylesheet, JavaScript, and HEAD data of this htmlbox as JSON"
},
{
"moduleid": 1338,
"plugin": "htmlbox",
"action": "set_content",
"description": "Updates HTML content, custom CSS styles, JavaScript, and/or HEAD data of this htmlbox..."
}
]
}
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 636 list_items limit=2 fields=*
Action 'list_items' on module #636 executed successfully.
Response: {
"total": 67,
"count": 2,
"offset": 0,
"items": [
{
"id": 11513,
"url": "/?section=4505&language=el_GR&itemid706=11513&detail706=1",
"section": 4505,
"p_photos-683": [],
"p_group-762": false,
"p_pricevalue-834": 0,
"p_pricevalue-1381-1374": 0,
"p_pricevalue-1381-1375": 0,
"p_group_add-1448": [],
"p_inputbox-1190": "",
"p_availability-1198": 0,
"p_textbox-685": "Του ειπα 1500 με 2.000 ευρω το σιτε + 300 euro τα social το μηνα για 4 post...",
"p_group-1201": false
},
{
"id": 6279,
"url": "/?section=4505&language=el_GR&itemid706=6279&detail706=1",
"section": 4505,
"p_photos-683": [],
"p_group-762": false,
"p_pricevalue-834": 2200,
"p_pricevalue-1381-1374": 0,
"p_pricevalue-1381-1375": 0,
"p_group_add-1448": [],
"p_inputbox-1190": "",
"p_availability-1198": 0,
"p_textbox-685": "2200 site (περιλαμβάνει ΑΜΕΑ - Αγγλικά - και φιλοξενία 500€)",
"p_group-1201": false
}
],
"field_labels": {
"section": "Κατηγορία",
"p_photos-683": "Φωτογραφία",
"p_group-762": "Προσθήκη στην Πρώτη Σελίδα",
"p_pricevalue-834": "Τιμή",
"p_pricevalue-1381-1374": "Έξτρα κόστος αποστολής - ΕΛΤΑ (Ελλάδα)",
"p_pricevalue-1381-1375": "Έξτρα κόστος αποστολής - Courier (Ελλάδα)",
"p_group_add-1448": "Κατασκευαστής",
"p_inputbox-1190": "Κωδικός Προϊόντος",
"p_availability-1198": "Διαθεσιμότητα",
"p_textbox-685": "Περιγραφή",
"p_group-1201": "Προσθήκη στο Wishlist"
}
}
Real Usage Walkthroughs (Actual Prompts, Tool Calls & Responses)
Below are three end-to-end execution traces captured from actual agent sessions on managedev.netsystem.gr (from /var/lib/ns-ai-daemon/.gemini/antigravity-cli/brain/ and live executions). Expand any trace to inspect the exact commands and responses.
TRACE 01 · Brain 15c5b07d
Creating & Populating a Multi-Photo News Article in items #15305 + Live Browser Verification
User Prompt: "Φτιάξε ένα νέο άρθρο στα Νέα με το ρεπορτάζ και τις φωτογραφίες του γάμου και έλεγξέ το στον browser."
# Step 1: Agent introspects items module #15305
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 15305 describe
Action 'describe' on module #15305 executed successfully.
Response: {"module_id":15305,"title":"Νέα - News","can_edit":true,"can_delete":true,"fields":[...]}
# Step 2: Agent inspects existing articles to see how fields (p_date-15297, p_textbox-15298, p_photos-15295, p_photos-15301) are structured
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 15305 list_items limit=2
Action 'list_items' on module #15305 executed successfully.
Response: {"total":3,"count":2,"offset":0,"items":[{"id":15333,"url":"\\/?section=7&language=el_GR&itemid15305=15333&detail15305=1"},{"id":15318,"url":"\\/?section=7&language=el_GR&itemid15305=15318&detail15305=1"}]}
# Step 3: Agent creates the new item via save_item, passing external/local image URLs to p_photos-15295 and p_photos-15301
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 15305 save_item \\
'values={"name":"Ο γάμος της Ελισάβετ Μπάρλου και του Γιώργου Σγούρα που έγινε viral στην Πάτρα","p_date-15297":"2026-10-14","p_textbox-15298":"Μια λαμπερή βραδιά γεμάτη αγάπη...
","p_textbox-15418":"Μια βραδιά που έφτασε μέχρι τον ουρανό
...","p_textbox-15420":"Ένας γάμος με αλήθεια
...","p_textbox-15300":"Ο γάμος της Ελισάβετ Μπάρλου...
","p_photos-15295":["https://www.thebest.gr/.../1.jpg","https://www.thebest.gr/.../2.jpg"],"p_photos-15301":["https://www.thebest.gr/.../3.jpg"]}'
Action 'save_item' on module #15305 executed successfully.
Response: {
"status": "ok",
"id": 99627,
"item": {
"id": 99627,
"url": "/?section=7&language=el_GR&itemid15305=99627&detail15305=1",
"p_date-15297": "2026-10-14",
"p_photos-15295": [
{"id": 210, "url": "/cache/files/1721d3bc3c20efb520cc8aaeadaf53b6990ae6cd.jpg"},
{"id": 211, "url": "/cache/files/b313e7c3cda3a1627f23694b872ce5f400041558.jpg"}
],
"p_photos-15301": [
{"id": 212, "url": "/cache/files/f889061998230cac32539ddfabb0c077ed7727cc.jpg"}
]
}
}
# Step 4: Agent authenticates the isolated Chrome context and verifies the rendered article
$ /var/lib/ns-ai-daemon/browser.py managedev.netsystem.gr auth
Authenticated browser for managedev.netsystem.gr (3 cookies injected: PHPSESSID, permpin, token).
# Step 5: Agent calls chrome-devtools MCP:
# -> select_page(pageId=CMS_BROWSER_TARGET_ID)
# -> navigate_page(url="https://managedev.netsystem.gr/?section=7&language=el_GR&itemid15305=99627&detail15305=1")
# -> take_snapshot() / take_screenshot() to confirm all photos and typography render cleanly.
TRACE 02 · Brain bcd42267 & Live Section #100021
Creating a New Page in treemenu #10 & Publishing Interactive HTML/CSS/JS in htmlbox #341
Goal: Inspect existing theme & navigation tree, create a new top-level page under parent_id=734, and populate its htmlbox #341 with HTML, scoped CSS (.htmlbox341 ...), and interactive JavaScript.
# Step 1: Inspect site theme settings via sitelook #134
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 134 get_settings
Action 'get_settings' on module #134 executed successfully.
Response: {"section":0,"vars":{"bodybackcolor":{"value":"#f8fafd","css_var":"var(--bodybackcolor)"},"fontfamily":{"value":"'Google Sans', 'Roboto', -apple-system, sans-serif","css_var":"var(--fontfamily)"},...},"layout":"default","class":"771"}
# Step 2: Inspect menu structure via treemenu #10
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 10 get_tree depth=1
Action 'get_tree' on module #10 executed successfully.
Response: {"status":"ok","startpage":"734","parent_id":"734","tree":[...],"total_nodes":20}
# Step 3: Create the 'Chat walkthrough' page under parent_id=734 with layout=onecol and class=524
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 10 add_node \\
parent_id=734 'name={"el":"Chat walkthrough","en":"Chat walkthrough"}' \\
visible=1 published=1 layout=onecol class=524
Action 'add_node' on module #10 executed successfully.
Response: {"status":"ok","id":100021,"parent_id":734,"name":{"el":"Chat walkthrough","en":"Chat walkthrough"}}
# Step 4: Populate htmlbox #341 on section=100021 with HTML, scoped CSS, and JS
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 341 set_content \\
section=100021 lang=el content="...HTML..." css=".htmlbox341 .cw-root { ... }" js="..."
Action 'set_content' on module #341 executed successfully.
Response: {"status":"ok","moduleid":341,"section":100021,"language":"el","container_class":"htmlbox341","updated":["content","css","js"]}
TRACE 03 · Brain 7578dd2d
Processing User Attachments & Publishing Generated Files Back to Chat via share_file
Flow: When a user attaches an image or asks the agent to generate a downloadable file/image in chat:
# 1. User uploads an attachment in ns_ai_chat.js -> PHP chat_stream encodes it as base64
# 2. ns_ai_daemon.py materializes it at:
# /var/lib/ns-ai-daemon/projects/managedev.netsystem.gr/uploads/<stream_id>/<filename>
# and injects <attached_files> into the prompt.
# 3. To share a generated or processed file back in the chat bubble, the agent base64-encodes
# the file from its session tmp/<stream_id>/ workspace and calls share_file on the chat module:
$ B64=$(base64 -w 0 /var/lib/ns-ai-daemon/projects/managedev.netsystem.gr/tmp/$CMS_STREAM_ID/architecture-export.png)
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 21319 share_file \\
filename="architecture-export.png" content_base64="$B64"
Action 'share_file' on module #21319 executed successfully.
Response: {
"status": "ok",
"filename": "architecture-export.png",
"is_image": true,
"url": "https://managedev.netsystem.gr/cache/files/ai_shared/7cd66de9c26b910e58bf40d1d440326a/architecture-export.png",
"markdown": ""
}
# 4. The agent includes the returned `markdown` in its response text, and ns_ai_chat.js renders it inline!
TRACE 04 · CMS-Native Page Composition
Composing a Custom Page with _pagemodules, items%gallery_w_caption, Cloned forms, and site_images/
Goal: Build a custom page using native CMS modules instead of monolithic raw HTML/JS: insert a dedicated htmlbox, create a native PhotoSwipe gallery (items%gallery_w_caption) and upload photos + captions into the CMS, clone an existing contact forms module, and manage theme assets in site_images/.
# Step 1: Inspect page modules & layout positions via kernel module _pagemodules
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr _pagemodules list_page_modules section=25627
# Step 2: Add a fresh standalone htmlbox & populate its content (auto-defaults to parent page section)
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr _pagemodules add_module \
section=25627 module=htmlbox position=rightytop
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 100255 set_content \
content="<div class='intro'>...</div>"
# Step 3: Add a native CMS photo gallery (items%gallery_w_caption) with custom header_label
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr _pagemodules add_module \
section=25627 module=items%gallery_w_caption position=rightytop \
'options={"header_label":{"el":"Φωτογραφίες","en":"Photos"}}'
# -> Returns module_id=100256 and fields: ["p_photos-100266", "p_inputbox-100270", "section"]
# Step 4: Upload photos (via URL or @filepath) & captions into the gallery
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 100256 save_item \
p_photos-100266=@/var/lib/ns-ai-daemon/projects/managedev.netsystem.gr/tmp/$CMS_STREAM_ID/photo1.png \
"p_inputbox-100270=Δοκιμαστική Λεζάντα"
# Step 5: Discover existing forms modules & clone forms #119 onto the page
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr _pagemodules find_modules module=forms
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr _pagemodules add_module \
section=25627 clone_id=119 position=rightytop
# Step 6: Upload or inspect theme assets in site_images/ via sitelook #134
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 134 list_site_images
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 134 upload_site_image \
name=logo.png file=@/var/lib/ns-ai-daemon/projects/managedev.netsystem.gr/tmp/$CMS_STREAM_ID/logo.png
Prompt Engineering Audit & Improvements (6-Axis Framework & 24 Fixes)
By auditing the prompt-generating code across localib/libmod.php, modules/ns_ai_chat.php, modules/registry/*_reg.php, and /usr/local/bin/ns_ai_daemon.py alongside real execution transcripts in /var/lib/ns-ai-daemon/.gemini/antigravity-cli/brain/, we defined a 6-axis framework for prompt quality and resolved 24 concrete issues.
1. Defining "Prompt Improvement" (6 Measurable Axes)
Zero stale action names (e.g. set_content instead of removed htmlbox_change), clean page URLs via getURL(0) instead of leaking /action/21319/stream_chat, and filtering optional_modules against already-configured modules.
Explicit rules distinguishing per-page htmlbox instances (scope: page-specific) from global htmlbox instances (scope: global all-pages), editable sitelook variables vs. read-only metavars, and public site DOM vs. the #adminbar CMS toolbar.
Eliminating 2x–5x intra-prompt repetition in items_reg.php, sitelook_reg.php, and htmlbox_reg.php, and replacing full prompt re-injection on follow-up turns with build_follow_up_instructions() (-88.4% size on Turn 2+).
Providing fallback getHumanID() titles and htmlbox scope/layer traits upfront, exact Chrome DevTools MCP parameter names (filePath, function, targetId), and noting that tmp/<stream_id>/ is pre-created.
Replacing the misleading "use curl to download pages" instruction with a strict rule to use cms_call.py and Chrome DevTools for the target domain, and explicitly forbidding reading /vhosts/ source files.
Including treemenu, items, and siteconfig in the site_design preset's optional_modules and auto-including _pagemodules for CMS-native page/module composition.
2. Summary of the 24 Prompt & Runtime Fixes
| # | Component | Observed Defect (Before) | Implemented Fix (After) |
|---|---|---|---|
| 01 | ns_ai_daemon.py |
4 command examples instructed the agent to run the removed htmlbox_change action. |
Updated all examples in build_system_instructions() to use set_content. |
| 02 | localib/libmod.php |
ns_ai_build_context() used getSiteURI(), which during stream_chat resolved to /action/21319/stream_chat?section=21316. |
Replaced getSiteURI() with str_replace('&', '&', getURL(0)), stripping tmpvars / /action/... and resolving the clean page URL. |
| 03 | ns_ai_daemon.py |
optional_modules was not filtered against configured_module_names, falsely telling the agent htmlbox was unconfigured. |
Filtered optional_modules against configured_module_names so only unconfigured optional modules (e.g. siteconfig) are listed. |
| 04 | ns_ai_daemon.py |
Line 289 told the agent to "use curl to download pages", causing it in transcript 7e5a709f to scrape CMS HTML via curl | grep. |
Instructed the agent to always use cms_call.py and Chrome DevTools for the CMS domain and never scrape it with curl. |
| 05 | libmod.php & ns_ai_chat.php |
In transcript 6a8a2658, the agent wrote page-specific CSS/JS into global modules #15434/#15435 instead of page-scoped #341. |
Annotated every htmlbox instance with [scope: page-specific | global all-pages; layers: ...] and added explicit per-page vs. global rules. |
| 06 | ns_ai_chat.php & ns_ai_daemon.py |
In transcript 5dc5aecf, after running browser.py auth, the agent mistook #adminbar for the public site menu and styled it. |
Added explicit rules in both site_design and browser guidelines that #adminbar is the CMS admin toolbar and must be ignored. |
| 07 | ns_ai_daemon.py |
Wrote ambiguous file=... or filePath=..., omitted session_target_id, and didn't mention HttpOnly cookies (wasting 2–4 tool calls per run). |
Specified exact MCP parameter names (filePath, function), injected targetId, and documented HttpOnly cookie behavior. |
| 08 | ns_ai_daemon.py |
Re-prepended the full ~12.8 KB system prompt on every turn of multi-turn conversations (e.g. 17 times in transcript 8efc1ed0). |
Added build_follow_up_instructions() for conversation_id turns, reducing follow-up prompt size from ~12.8 KB to ~1.5 KB (-88.4%). |
| 09 | modules/registry/*_reg.php |
items_reg.php duplicated parameter signatures in description and produced .. Parameters:; sitelook/htmlbox repeated boilerplate. |
Deduplicated items, sitelook, htmlbox, and siteconfig registry descriptions and stripped trailing periods in the daemon. |
| 10 | ns_ai_daemon.py |
Did not mention that tmp/<stream_id>/ is pre-created by the daemon, causing the agent to run redundant mkdir -p tool calls. |
Explicitly noted that tmp/<stream_id>/ is already pre-created (do NOT run mkdir -p). |
| 11 | modules/ns_ai_chat.php |
site_design preset omitted treemenu, items, and siteconfig from optional_modules and preset instructions. |
Added treemenu, items, and siteconfig to site_design with workflow guidance (e.g. using add_node's returned id as section). |
| 12 | modules/htmlbox.php |
In live trace e636546b (Steps 2, 40–44), get_content on global htmlbox #15434 returned section: 7 (empty) because index.php sets $_GET['section'] = 7, bypassing showtoallsections=1. |
Updated htmlbox::get_content() so showtoallsections=1 resolves to section: 0 (matching set_content) when no explicit section is passed, and always includes the layer key on single-layer boxes. |
| 13 | ns_ai_daemon.py (ChromeManager) |
In trace e636546b (Steps 5–10, 35–39, 155–169), navigate_page(url=...) failed without integer pageId, and list_pages() exposed a shared default-profile tab (pageId=1) with stale cookies/localStorage alongside the isolated tab. |
create_session_context() now closes any non-isolated default-profile tab while a session is active (so pageId=1 is guaranteed to be the isolated about:blank#<session_id> tab) and documents pageId=1 on all MCP tool signatures. |
| 14 | modules/ns_ai_chat.php |
In trace e636546b (Steps 21, 49–54), the rule to scope all CSS under .htmlbox<id> contradicted global CSS-only boxes (#15434, which has no HTML wrapper), and the agent spent 6 steps checking why CSS was in /cache/files/<hash>.css instead of inline <style>. |
Distinguished HTML-bearing component boxes (scope with .htmlbox<id>) from global CSS-only boxes (target site-wide or .ns-... selectors directly) and documented /cache/files/<hash>.css bundling. |
| 15 | cms_call.py & ns_ai_daemon.py |
In trace e636546b (Steps 23, 67, 100, 137, 158, 174), because cms_call.py lacked @filepath support, the agent read cms_call.py and regenerated a 542-line Python wrapper script 5 times (~80 KB of output tokens). |
Added key=@filepath support to cms_call.py (e.g. css=@tmp/<session>/style.css) and instructed the agent to edit raw .html/.css/.js files with replace_file_content. |
| 16 | ns_ai_daemon.py |
In trace e636546b (Steps 93, 130, 151, 188), agy's base system prompt caused the agent to output broken [htmlbox #15434](file:///var/lib/ns-ai-daemon/...) links in web chat responses. |
Added a strict RESPONSE FORMATTING RULE forbidding file:///... links and internal /var/lib/ns-ai-daemon/... paths in user-facing web chat replies. |
| 17 | ns_ai_daemon.py |
In trace e636546b (Steps 55–64), the agent made 5 sequential evaluate_script round-trips to inspect one level of DOM .children at a time. |
Added a browser efficiency guideline to combine DOM hierarchy and computed-style inspections into a single evaluate_script call. |
| 18 | modules/ns_ai_chat.php & treemenu_reg.php |
In trace 6dcef567 (Steps 48–64), htmlbox #341 did not render on a page with bare skeleton class 772 (Δύο στήλες), and the vague canhavecontent description misled the agent into thinking canhavecontent=0 blocked page content. |
Added a full explanation of Page Types (class) and inheritance, a reference list of common 2-column Page Types (524, 1386, 1542, 1519, 1515, 1517 grid layout, and base skeleton classes 1/49/771/772), a note on class-level treemenu instances, and clarified that canhavecontent only applies to parent menu items with child sub-pages (typically 0 = use menu default). |
| 19 | ns_ai_daemon.py |
In trace 6dcef567 (Steps 5–38), scraping an external Elementor site via evaluate_script returning raw innerHTML/outerHTML exceeded tool output limits and spilled to disk 8 times in a row (+16 steps). |
Instructed the agent to return compact structured data objects (headings, copy, image/background URLs, links, form fields, styles) from evaluate_script and never return raw document.body.innerHTML or oversized outerHTML strings (>4 KB). |
| 20 | ns_ai_daemon.py |
In trace 6dcef567 (Steps 93–110, 145–147), below-the-fold <img loading="lazy"> tags remained unloaded during take_screenshot(fullPage=true) (causing 14 debugging steps), and handle_dialog was called with accept=true instead of action="accept". |
Documented the headless Chrome loading="lazy" behavior on full-page screenshots and added handle_dialog(pageId=1, action="accept"|"dismiss") to the MCP tool reference. |
| 21 | _pagemodules.php, _pagemodules_reg.php & layout_func.php |
In trace 6dcef567, the agent had no AI actions to inspect, add, clone, configure, reorder, or delete modules on a page, forcing it to cram custom HTML/JS galleries and forms into a single shared htmlbox #341. |
Exposed 7 AI actions on kernel module _pagemodules (list_page_modules, find_modules, add_module, get_module_options, set_module_options, reorder_module, delete_module), auto-included in the site_design preset and routable via cms_call.py <domain> _pagemodules <action>. |
| 22 | items.php, base_func.php, p_photos.php & p_inputbox.php |
Standalone items%gallery_w_caption modules have useparentitemidascat=1 (which broke aiFields/save_item when no parent items existed) and minshowlevel=999 on caption p_inputbox (hiding it from ai_fields). |
Added items::usesParentItemAsCat() (checking is_object($this->parent)), exposed edit-only fields when $this->canEdit() is true, added base64 data-URI (@filepath) image upload in p_photos, and supported partial-language syncwithitemname updates in p_inputbox::saveNode(). |
| 23 | sitelook.php, sitelook_reg.php & cms_call.py |
The agent had no way to inspect or upload theme/layout images in site_images/ (such as logo.png or background assets) when styling a site. |
Added list_site_images, upload_site_image (accepting HTTP(S) URLs or local @filepath images via cms_call.py base64 data-URI encoding), and delete_site_image to sitelook. |
| 24 | htmlbox.php & ns_ai_chat.php |
Newly created standalone htmlbox modules on a page defaulted to $homepage (7) when called via cms_call.py without an explicit section parameter. |
Updated htmlbox::getSection() and htmlbox::get_content() to default to the module's parent page ID when $homepage is true, and added CMS-native page composition, curated module names, and layout position guidance to site_design. |