OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-atomspace-storage / opencog / persist / json / JSCommands.cc

632 lines · cc ref:
/*
 * JSCommands.cc
 * Fast command interpreter for basic JSON AtomSpace commands.
 *
 * Copyright (C) 2020, 2021, 2022 Linas Vepstas
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License v3 as
 * published by the Free Software Foundation and including the exceptions
 * at http://opencog.org/wiki/Licenses
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program; if not, write to:
 * Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <opencog/atoms/atom_types/NameServer.h>
#include <opencog/atoms/base/Link.h>
#include <opencog/atoms/base/Node.h>
#include <opencog/atoms/parallel/TriggerLink.h>
#include <opencog/atoms/value/FloatValue.h>
#include <opencog/atomspace/AtomSpace.h>
#include <opencog/atomspace/version.h>

#include "JSCommands.h"
#include "Json.h"
#include <opencog/persist/sexpr/Sexpr.h>

#include <time.h>

#include <algorithm>
#include <functional>
#include <iomanip>
#include <string>

using namespace opencog;

// Helper function to parse boolean parameters from commands
static bool parse_bool_param(const std::string& cmd, size_t& pos, size_t epos, bool js_mode)
{
	bool recursive = false;
	if (js_mode) {
		pos = cmd.find_first_not_of(",) \n\t", pos);
		if (std::string::npos != pos) {
			recursive = true;
			if (0 == cmd.compare(pos, 1, "0") or
			    0 == cmd.compare(pos, 5, "false") or
			    0 == cmd.compare(pos, 5, "False"))
				recursive = false;
		}
	} else {
		// In MCP mode, look for "subclass" or "recursive" field
		// Search backwards from epos to find the boolean
		size_t bool_pos = cmd.rfind("\"subclass\"", epos);
		if (std::string::npos == bool_pos)
			bool_pos = cmd.rfind("\"recursive\"", epos);
		if (std::string::npos != bool_pos) {
			bool_pos = cmd.find(':', bool_pos);
			if (std::string::npos != bool_pos && bool_pos < epos) {
				bool_pos = cmd.find_first_not_of(": \n\t", bool_pos);
				if (std::string::npos != bool_pos && bool_pos < epos) {
					if (0 == cmd.compare(bool_pos, 4, "true"))
						recursive = true;
				}
			}
		}
	}
	return recursive;
}

static std::string reterr(const std::string& cmd)
{
	return "{\"content\": [{\"type\":\"text\", \"text\": \"Error: Invalid Request - " + cmd + "\"}], \"isError\": true}\n";
}

static std::string retmsgerr(const std::string& errmsg)
{
	return "{\"content\": [{\"type\":\"text\", \"text\": \"Error: Invalid params - " + errmsg + "\"}], \"isError\": true}\n";
}

// MCP tool responses use the "content" format.
// All responses (success or error) return text content.
// Simple values can be returned as quoted strings.
#define RETURN(RV) { \
	return "{\"content\": [{\"type\":\"text\", \"text\": \"" RV "\"}]}\n"; }

// For complex results (JSON structures), we need to escape quotes
// and wrap the JSON as a text string within the content array.
#define RETURNSTR(RV) { \
	std::string srv(RV); \
	std::replace(srv.begin(), srv.end(), '\n', ' '); \
	std::stringstream ss; \
	ss << std::quoted(srv); \
	std::string rs = "{\"content\": [{\"type\":\"text\", \"text\": "; \
	rs += ss.str(); \
	rs += "}]}\n"; \
	return rs; \
	}

// Common boilerplate
#define CHK_FOR_JSON_ARG \
	size_t brace = cmd.find_first_not_of(" \n\t", pos); \
	bool is_json_object = (brace != std::string::npos and cmd[brace] == '{');

#define GET_TYPE \
	Type t = NOTYPE; \
	try { \
		t = Json::decode_type_arg(cmd, pos); \
	} catch(...) { \
		return retmsgerr("Unknown type: " + cmd.substr(pos)); \
	}

#define GET_BOOL \
	bool recursive = parse_bool_param(cmd, pos, epos, js_mode);

#define GET_ATOM(rv) \
	Handle h = Json::decode_atom(cmd, pos, epos); \
	if (nullptr == h) return reterr(cmd); \
	h = as->get_atom(h); \
	if (nullptr == h) RETURN(rv);

#define ADD_ATOM \
	Handle h = Json::decode_atom(cmd, pos, epos); \
	if (nullptr == h) return reterr(cmd); \
	h = as->add_atom(h); \
	if (nullptr == h) return retmsgerr("No such Atom");

#define GET_KEY \
	pos = cmd.find("\"key\":", epos); \
	if (std::string::npos == pos) return reterr(cmd); \
	pos += 6; \
	epos = cmd.size(); \
	Handle k = Json::decode_atom(cmd, pos, epos); \
	if (nullptr == k) return reterr(cmd); \
	k = as->add_atom(k); \
	pos = cmd.find(',', epos); \
	if (std::string::npos == pos) return retmsgerr("No such Key");

#define GET_VALUE \
	pos = cmd.find("\"value\":", pos); \
	if (std::string::npos != pos) \
		pos += 8; \
	else { \
		pos = cmd.find("\"values\":", pos); \
		if (std::string::npos == pos) return reterr(cmd); \
		pos += 9; \
	} \
	epos = cmd.size(); \
	ValuePtr v = Json::decode_value(cmd, pos, epos); \
	if (nullptr == v) return reterr(cmd);

/// The cogserver provides a network API to send/receive Atoms, encoded
/// as JSON, over the internet. This is NOT as efficient as the
/// s-expression API, but is more convenient for web developers.
//
std::string JSCommands::interpret_command(AtomSpace* as,
                                          const std::string& cmd)
{
	// Fast dispatch. There should be zero hash collisions
	// here. If there are, we are in trouble. (Well, if there
	// are collisions, just prepend a dot?)
	static const size_t versn = std::hash<std::string>{}("version");
	static const size_t gtatm = std::hash<std::string>{}("getAtoms");
	static const size_t gtsub = std::hash<std::string>{}("getSubTypes");
	static const size_t gtsup = std::hash<std::string>{}("getSuperTypes");
	static const size_t haven = std::hash<std::string>{}("haveNode");
	static const size_t havel = std::hash<std::string>{}("haveLink");
	static const size_t havea = std::hash<std::string>{}("haveAtom");
	static const size_t makea = std::hash<std::string>{}("makeAtom");
	static const size_t gtinc = std::hash<std::string>{}("getIncoming");
	static const size_t gtkey = std::hash<std::string>{}("getKeys");
	static const size_t gtval = std::hash<std::string>{}("getValues");
	static const size_t gtvak = std::hash<std::string>{}("getValueAtKey");
	static const size_t stval = std::hash<std::string>{}("setValue");
	static const size_t execu = std::hash<std::string>{}("execute");
	static const size_t extra = std::hash<std::string>{}("extract");
	static const size_t rpcnt = std::hash<std::string>{}("reportCounts");

	// Ignore comments, blank lines
	size_t cpos = cmd.find_first_not_of(" \n\t");
	if (std::string::npos == cpos) return "";
	if ('/' == cmd[cpos]) return "";
	if ('#' == cmd[cpos]) return "";

	// In js-mode, the commands are all of the form
	//    AtomSpace.someCommand(args)
	// In MCP-mode, the commands are all of the form
	//    { "tool": "someToolName", "params": { args }}
	// So if we find an open brace at the start of the string,
	// then we are in MCP-mode. Else we are in JS-mode.

	bool js_mode = true;

	if ('{' == cmd[cpos])
		js_mode = false;
	else if ('A' != cmd[cpos])
		return reterr(cmd);

	size_t pos, epos;
	size_t act;

	if (js_mode)
	{
		cpos = cmd.find_first_of(".", cpos);
		if (std::string::npos == cpos) return reterr(cmd);

		cpos = cmd.find_first_not_of(". \n\t", cpos);
		if (std::string::npos == cpos) return reterr(cmd);

		epos = cmd.find_first_of("( \n\t", cpos);
		if (std::string::npos == epos) return reterr(cmd);
		pos = epos + 1;

		act = std::hash<std::string>{}(cmd.substr(cpos, epos-cpos));
		epos = cmd.size();
	}
	else
	{
		cpos = cmd.find("\"tool\":", cpos);
		if (std::string::npos == cpos) return reterr(cmd);
		cpos += 7; // 7 == strlen("\"tool\":");

		cpos = cmd.find_first_not_of("\" \n\t", cpos);
		if (std::string::npos == cpos) return reterr(cmd);

		size_t tool_end = cmd.find_first_of("\", \n\t", cpos);
		if (std::string::npos == tool_end) return reterr(cmd);

		// Extract the tool name
		std::string tool_name = cmd.substr(cpos, tool_end-cpos);
		act = std::hash<std::string>{}(tool_name);

		// OK, so now pos and tool_end bracket the tool name.
		// Advance cpos past the params.
		pos = cmd.find("\"params\":", tool_end);
		if (std::string::npos == pos) return reterr(cmd);
		pos += 9; // 9 == strlen("\"params\":");

		// For MCP mode, find the closing } of the params object
		// We need to find the matching } for the { that comes after "params":
		int brace_count = 0;
		size_t scan_pos = pos;
		epos = std::string::npos;

		while (scan_pos < cmd.size()) {
			if (cmd[scan_pos] == '{') {
				brace_count++;
			} else if (cmd[scan_pos] == '}') {
				brace_count--;
				if (brace_count == 0) {
					epos = scan_pos + 1; // Include the closing }
					break;
				}
			}
			scan_pos++;
		}

		if (std::string::npos == epos) return reterr(cmd);
	}

	// -----------------------------------------------
	// Get version
	// AtomSpace.version()
	// AtomSpace.version({})
	if (versn == act)
	{
		RETURN(ATOMSPACE_VERSION_STRING);
	}

	// -----------------------------------------------
	// Get subtypes of the named type.
	// AtomSpace.getSubTypes("Link")
	// AtomSpace.getSubTypes("Link", true)
	// AtomSpace.getSubTypes({ "type": "Link"})
	// AtomSpace.getSubTypes({ "type": "Link", "recursive": true})
	if (gtsub == act)
	{
		GET_TYPE;
		GET_BOOL;

		std::vector<Type> vect;
		if (recursive)
			nameserver().getChildrenRecursive(t, std::back_inserter(vect));
		else
			nameserver().getChildren(t, std::back_inserter(vect));
		RETURNSTR(Json::encode_type_list(vect));
	}

	// -----------------------------------------------
	// Get supertypes of the named type.
	// AtomSpace.getSuperTypes("ListLink")
	// AtomSpace.getSuperTypes("ListLink", true)
	// AtomSpace.getSuperTypes({ "type": "ListLink", "recursive": true})
	// AtomSpace.getSuperTypes({ "type": "ListLink"})
	if (gtsup == act)
	{
		GET_TYPE;
		GET_BOOL;

		std::vector<Type> vect;
		if (recursive)
			nameserver().getParentsRecursive(t, std::back_inserter(vect));
		else
			nameserver().getParents(t, std::back_inserter(vect));
		RETURNSTR(Json::encode_type_list(vect));
	}

	// -----------------------------------------------
	// AtomSpace.getAtoms("Node") // no subclassing
	// AtomSpace.getAtoms("Node", true)
	// AtomSpace.getAtoms({"type": "Node"}) // no sublassing
	// AtomSpace.getAtoms({"type": "Node", "subclass": true})
	if (gtatm == act)
	{
		GET_TYPE;
		GET_BOOL;

		HandleSeq hset;
		as->get_handles_by_type(hset, t, recursive);
		std::string rv;
		if (js_mode)
		{
			// JSON format: array of atoms
			rv = "[\n";
			bool first = true;
			for (const Handle& h: hset)
			{
				if (not first) { rv += ",\n"; } else { first = false; }
				rv += Json::encode_atom(h, "  ");
			}
			rv += "]";
		}
		else
		{
			rv = "(list ";
			for (const Handle& h: hset)
				rv += Sexpr::encode_atom(h);
			rv += ")";
		}
		RETURNSTR(rv);
	}

	// -----------------------------------------------
	// AtomSpace.haveNode("Concept", "foo")
	// AtomSpace.haveNode({ "type": "Concept", "name": "foo"})
	if (haven == act)
	{
		CHK_FOR_JSON_ARG;
		if (is_json_object)
		{
			GET_ATOM("false")
			RETURN("true");
		}

		// Function argument format: type followed by name
		GET_TYPE;

		if (not nameserver().isA(t, NODE))
			return retmsgerr("Type is not a Node type: " + cmd.substr(epos));

		pos = cmd.find_first_not_of(",) \n\t", pos);
		std::string name = Json::get_node_name(cmd, pos, epos);
		Handle h = as->get_node(t, std::move(name));

		if (nullptr == h) RETURN("false");
		RETURN("true");
	}

	// -----------------------------------------------
	// AtomSpace.haveLink("List", [{ "type": "ConceptNode", "name": "foo"}])
	// AtomSpace.haveLink({ "type": "List", "outgoing": [{ "type": "ConceptNode", "name": "foo"}]})
	if (havel == act)
	{
		CHK_FOR_JSON_ARG;
		if (is_json_object)
		{
			GET_ATOM("false")
			RETURN("true");
		}

		// Function argument format: type followed by outgoing array
		GET_TYPE;

		if (not nameserver().isA(t, LINK))
			return retmsgerr("Type is not a Link type: " + cmd.substr(epos));

		pos = cmd.find_first_not_of(", \n\t", pos);
		// Skip the opening '[' if present
		if (pos < cmd.size() && cmd[pos] == '[') pos++;

		HandleSeq hs;

		size_t l = pos;
		size_t r = epos;
		while (std::string::npos != r)
		{
			l = cmd.find_first_not_of(" \n\t", l);
			if (l == std::string::npos || cmd[l] == ']') break;

			Handle ho = Json::decode_atom(cmd, l, r);
			if (nullptr == ho) RETURN("false");
			hs.push_back(ho);

			// Look for the comma
			l = cmd.find(",", r);
			if (std::string::npos == l) break;
			l ++;
			r = epos;
		}
		Handle h = as->get_link(t, std::move(hs));

		if (nullptr == h) RETURN("false");
		RETURN("true");
	}

	// -----------------------------------------------
	// AtomSpace.haveAtom({ "type": "ConceptNode", "name": "foo"})
	if (havea == act)
	{
		GET_ATOM("false");
		RETURN("true");
	}

	// -----------------------------------------------
	// AtomSpace.makeAtom({ "type": "ConceptNode", "name": "foo"})
	// AtomSpace.makeAtom(
	//    {"outgoing":
	//       [{"type":"Concept","name":"start"},
	//       {"type":"Concept","name":"finish"}],
	//    "type":"EdgeLink"})
	if (makea == act)
	{
		ADD_ATOM;
		RETURN("true");
	}

	// -----------------------------------------------
	// AtomSpace.getIncoming({"type": "Concept", "name": "foo"})
	// AtomSpace.getIncoming({"type": "Concept", "name": "foo"}, "Evaluation")
	if (gtinc == act)
	{
		GET_ATOM("[]");

		Type t = NOTYPE;
		pos = cmd.find(",", epos);
		if (std::string::npos != pos)
		{
			pos++;
			try {
				t = Json::decode_type(cmd, pos);
			}
			catch(...) {
				return retmsgerr("Unknown type: " + cmd.substr(pos));
			}
		}

		IncomingSet is;
		if (NOTYPE != t)
			is = h->getIncomingSetByType(t);
		else
			is = h->getIncomingSet();

		std::string atmlist;
		if (js_mode)
		{
			bool first = true;
			atmlist = "[";
			for (const Handle& hi : is)
			{
				if (not first) { atmlist += ",\n"; } else { first = false; }
				atmlist += Json::encode_atom(hi, "");
			}
			atmlist += "]";
		}
		else
		{
			atmlist = "(list ";
			for (const Handle& hi : is)
				atmlist += Sexpr::encode_atom(hi);
			atmlist += ")";
		}
		RETURNSTR(atmlist);
	}

	// -----------------------------------------------
	// AtomSpace.getKeys({ "type": "ConceptNode", "name": "foo"})
	if (gtkey == act)
	{
		GET_ATOM("[]");
		HandleSet keys = h->getKeys();

		std::string klist;
		if (js_mode)
		{
			bool first = true;
			klist = "[";
			for (const Handle& key : keys)
			{
				if (not first) { klist += ",\n"; } else { first = false; }
				klist += Json::encode_atom(key, "");
			}
			klist += "]";
		}
		else
		{
			klist = "(list ";
			for (const Handle& key : keys)
				klist += Sexpr::encode_atom(key);
			klist += ")";
		}
		RETURNSTR(klist);
	}

	// -----------------------------------------------
	// AtomSpace.getValues({ "type": "ConceptNode", "name": "foo"})
	if (gtval == act)
	{
		GET_ATOM("[]");
		std::string result = js_mode ? Json::encode_atom_values(h) : Sexpr::encode_atom_values(h);
		RETURNSTR(result);
	}

	// -----------------------------------------------
	// AtomSpace.getValueAtKey({ "type": "ConceptNode", "name": "foo",
	//                           "key": { "type": "PredicateNode", "name": "keewee" } })
	// Returns the value at the specified key for the given atom
	if (gtvak == act)
	{
		size_t save_pos = pos;
		ADD_ATOM;
		epos = save_pos;
		GET_KEY;

		// Get the value at the key
		ValuePtr v = h->getValue(k);
		if (nullptr == v) RETURN("null");

		std::string result = js_mode ? Json::encode_value(v) : Sexpr::encode_value(v);
		RETURNSTR(result);
	}

	// -----------------------------------------------
	// AtomSpace.setValue({ "type": "ConceptNode", "name": "foo",
	//     "key": { "type": "PredicateNode", "name": "keewee" },
	//     "value": { "type": "FloatValue", "value": [1, 2, 3] } } )
	// If all is well, then the member fields can occur in arbitrary
	// order, so that the key can be given before the name, which can
	// be given before the type.
	if (stval == act)
	{
		size_t save_pos = pos;
		ADD_ATOM;
		epos = save_pos;
		GET_KEY;
		GET_VALUE;

		as->set_value(h, k, v);
		RETURN("true");
	}

	// -----------------------------------------------
	// AtomSpace.execute({ "type": "PlusLink", "outgoing":
	//     [{ "type": "NumberNode", "name": "2" },
	//      { "type": "NumberNode", "name": "2" }] })
	if (execu == act)
	{
		// try..catch is needed because the Atom might be a TriggerLink
		// which will force execution, but then throw an exception with
		// the result of execution in it.
		ValuePtr vp;
		try
		{
			ADD_ATOM;
			vp = h->execute();
		}
		catch (const ValueReturnException& ex)
		{
			vp = ex._value;
		}
		std::string result = Json::encode_value(vp);
		RETURNSTR(result);
	}

	// -----------------------------------------------
	// AtomSpace.extract({ "type": "Concept", "name": "foo"}, true)
	if (extra == act)
	{
		Handle h = Json::decode_atom(cmd, pos, epos);
		if (nullptr == h) RETURN("false");
		pos = epos;
		GET_BOOL;
		bool ok = as->extract_atom(h, recursive);
		if (ok) RETURN("true");
		RETURN("false");
	}

	// -----------------------------------------------
	// AtomSpace.reportCounts()
	// Returns a list of atom types and their counts (non-zero only)
	if (rpcnt == act)
	{
		std::string rv = "{\n";
		bool first = true;

		// Get all types from the NameServer
		std::vector<Type> all_types;
		nameserver().getChildrenRecursive(ATOM, std::back_inserter(all_types));

		// Iterate through all types and get counts
		for (Type t : all_types)
		{
			size_t count = as->get_num_atoms_of_type(t, false);
			if (count > 0)
			{
				if (not first) { rv += ",\n"; } else { first = false; }
				rv += "  \"" + nameserver().getTypeName(t) + "\": " + std::to_string(count);
			}
		}
		rv += "\n}";
		RETURNSTR(rv);
	}

	// -----------------------------------------------
	return reterr(cmd);
}