Skip to content

Commit

Permalink
feat: 🚩 websocket server-based console ConsoleWeb
Browse files Browse the repository at this point in the history
- Optimized `ConsolePlus` and some fixes:
  - Fixed a syntax bug in 'Console.py'
  - Added "Output History" to record the output of NARS reasoner uses "output event handlers" in `Interface.py` (by enhances function `register_interface`)
- (NEW) ConsoleWeb: Set up a WebSocket-based server using package `websockets`
  - It gives ability to remote control `ConsolePlus` via WS connection "ws://[server address]:[server port]" (it can be set by user) and input Narsese/cmd, just like directly input on the command line.
  - In addition to receiving messages sent by clients as console input, it also returns the output of NARS reasoners as JSON text, which is in format `{"interface_name": str, "output_type": str, "content": str}`
    - an example output: `{"interface_name": "initial", "output_type": "ANSWER", "content": "<B-->C>. %1.000;0.900%"}`
  • Loading branch information
ARCJ137442 committed Oct 18, 2023
1 parent 4b49df0 commit 299cab9
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 39 deletions.
8 changes: 4 additions & 4 deletions Experiments/ExConsole/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ def list_variables() -> None:

@exp_register('r')
def list_histories() -> None:
print_history('')
print_history('1')
print_history_in('')
print_history_in('1')


@exp_register('help')
Expand Down Expand Up @@ -369,7 +369,7 @@ def load_JSON() -> None:
'Please enter a new interface name:') # accept the reasoner with a new interface
interface: NARSInterface = NARSInterface(
NARS=decoded_NAR) # create interface
reasoners[interface_name] = interface # directly add
interfaces[interface_name] = interface # directly add
reasoner_goto(interface_name) # goto
print(
f"Import a reasoner named {interface_name}, silent {'on' if interface.silent_output else 'off'}.")
Expand Down Expand Up @@ -428,7 +428,7 @@ def load_pickle() -> None:
'Please enter a new interface name: ')
# create interface
interface: NARSInterface = NARSInterface(NARS=decoded_NAR)
reasoners[interface_name] = interface # directly add
interfaces[interface_name] = interface # directly add
reasoner_goto(interface_name) # goto
print(
f"Import a reasoner named {interface_name}, silent {'on' if interface.silent_output else 'off'}.")
Expand Down
4 changes: 2 additions & 2 deletions pynars/Console.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ def handle_lines(nars: Reasoner, lines: str):
for task in tasks_derived: print_out(PrintType.OUT, task.sentence.repr(), *task.budget)

# while revising a judgement
if judgement_revised is not None: print_out(PrintType.OUT, judgement_revised.sentence.repr(),
if judgement_revised is not None: print_out(PrintType.OUT, judgement_revised.sentence.repr(),*judgement_revised.budget)

# while revising a goal *judgement_revised.budget)
# while revising a goal
if goal_revised is not None: print_out(PrintType.OUT, goal_revised.sentence.repr(), *goal_revised.budget)

# while answering a question for truth value
Expand Down
92 changes: 69 additions & 23 deletions pynars/ConsolePlus.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,14 +244,25 @@ def read_file(*args: List[str]) -> None:
current_NARS_interface.execute_file(path)


@cmd_register('history')
def print_history(*args: List[str]) -> None:
@cmd_register(('history', 'history-input'))
def print_history_in(*args: List[str]) -> None:
'''Format: history [... placeholder]
Output the user's input history
Default: The Narsese seen at the bottom of the system, not the actual input
User actual input cmd: input any parameters can be obtained'''
global _input_history
print('\n'.join(_input_history if args else current_NARS_interface.input_history))
global input_history
print('\n'.join(input_history if args else current_NARS_interface.input_history))


@cmd_register('history-output')
def print_history_out(*args: List[str]) -> None:
'''Format: history-output [... placeholder]
Output the console's output history
Default: The Narsese seen at the bottom of the system, not the actual input
User actual input cmd: input any parameters can be obtained'''
global output_history
for (interface_name, output_type, content) in output_history:
print(f'{interface_name}: [{output_type}]\t{content}')


@cmd_register(('execute', 'exec'))
Expand Down Expand Up @@ -409,21 +420,50 @@ def macro_repeat(name: str, num_executes: int) -> None:
for _ in range(num_executes):
macro_exec1(name=name)


interfaces: Dict[str, Reasoner] = {}
'The dictionary contains all registered NARS interfaces.'

# Reasoner management #


current_NARS_interface: NARSInterface = NARSInterface.construct_interface(
def register_interface(name: str, seed: int = -1, memory: int = 100, capacity: int = 100, silent: bool = False) -> NARSInterface:
'''
Wrapped from NARSInterface.construct_interface.
- It will auto register the new interface to `reasoners`
- It will add a output handler(uses lambda) to catch its output into output_history
'''
# create interface
interface = NARSInterface.construct_interface(
seed=seed,
memory=memory,
capacity=capacity,
silent=silent)
# add handler to catch outputs
global output_history
interface.output_handlers.append(
lambda out: output_history.append(
(name, # Name of interface e.g. 'initial'
out.type.name, # Type of output e.g. 'ANSWER'
out.content) # Content of output e.g. '<A --> B>.'
))
# register in dictionary
interfaces[name] = interface
# return the interface
return interface


current_NARS_interface: NARSInterface = register_interface(
'initial',
137,
500, 500,
silent=False)

reasoners: Dict[str, Reasoner] = {'initial': current_NARS_interface}


def current_nar_name() -> str:
global current_NARS_interface
for name in reasoners:
if reasoners[name] is current_NARS_interface:
for name in interfaces:
if interfaces[name] is current_NARS_interface:
return name
return None

Expand All @@ -442,11 +482,11 @@ def reasoner_list(*keywords: List[str]) -> None:
Enumerate existing reasoners; It can be retrieved with parameters'''
keywords = keywords if keywords else ['']
# Search for a matching interface name
reasoner_names: List[str] = prefix_browse(reasoners, *keywords)
reasoner_names: List[str] = prefix_browse(interfaces, *keywords)
# Displays information about "matched interface"
if reasoner_names:
for name in reasoner_names: # match the list of all cmds, as long as they match the search results - not necessarily in order
interface: NARSInterface = reasoners[name]
interface: NARSInterface = interfaces[name]
information: str = '\n\t'+"\n\t".join(f"{name}: {repr(inf)}" for name, inf in [
('Memory', interface.reasoner.memory),
('Channels', interface.reasoner.channels),
Expand All @@ -470,16 +510,17 @@ def reasoner_new(name: str, n_memory: int = 100, capacity: int = 100, silent: bo
Create a new reasoner and go to the existing reasoner if it exists
If an empty name is encountered, the default name is "unnamed"'''
global current_NARS_interface
if name in reasoners:
if name in interfaces:
print(
f'The reasoner exists! Now automatically go to the reasoner "{name}"!')
return (current_NARS_interface := reasoners[name])
return (current_NARS_interface := interfaces[name])

reasoners[name] = (current_NARS_interface := NARSInterface.construct_interface(
current_NARS_interface = register_interface(
name=name,
seed=current_NARS_interface.seed, # keep the seed until directly change
memory=n_memory,
capacity=capacity,
silent=silent
))
silent=silent)
print(
f'A reasoner named "{name}" with memory capacity {n_memory}, buffer capacity {capacity}, silent output {" on " if silent else" off "} has been created!')
return current_NARS_interface
Expand All @@ -490,9 +531,9 @@ def reasoner_goto(name: str) -> NARSInterface:
'''Format: reasoner-goto < name >
Transfers the current reasoner of the program to the specified reasoner'''
global current_NARS_interface
if name in reasoners:
if name in interfaces:
print(f"Gone to reasoner named '{name}'!")
return (current_NARS_interface := reasoners[name])
return (current_NARS_interface := interfaces[name])
print(f"There is no reasoner named '{name}'!")


Expand All @@ -501,12 +542,12 @@ def reasoner_delete(name: str) -> None:
'''Format: reasoner-select < name >
Deletes the reasoner with the specified name, but cannot delete the current reasoner'''
global current_NARS_interface
if name in reasoners:
if reasoners[name] is current_NARS_interface:
if name in interfaces:
if interfaces[name] is current_NARS_interface:
print(
f'Unable to delete reasoner "{name}", it is the current reasoner!')
return
del reasoners[name]
del interfaces[name]
print(f'the reasoner named "{name}" has been deleted!')
return
print(f'There is no reasoner named "{name}"!')
Expand All @@ -522,8 +563,13 @@ def random_seed(seed: int) -> None:
# Total index and other variables #

_parse_need_slash: bool = False
'Determines whether the last input is a command'

input_history: List[str] = []
'History of inputs'

_input_history: List[str] = []
output_history: List[Tuple[str, str, str]] = []
'History of outputs. The inner part is (Reasoner Name, PRINT_TYPE, Content)'

# Special grammar parser #

Expand Down Expand Up @@ -610,7 +656,7 @@ def execute_input(inp: str, *other_input: List[str]) -> None:

# add to history

_input_history.append(inp)
input_history.append(inp)

# pre-jump cmd
if inp.startswith('/'):
Expand Down
50 changes: 50 additions & 0 deletions pynars/ConsoleWeb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import websockets
import asyncio
from ConsolePlus import execute_input, output_history
from json import dumps


async def handler(websocket, path):
print(f"Connected with path={path}!")
messages2send = []
len_outputs = 0
last_output_json_obj = {}
async for message in websocket:
messages2send.clear()
# execute
execute_input(message)
# handle output using json
if len_outputs < len(output_history):
# traverse newer outputs
for i in range(len_outputs, len(output_history)):
# data: (interface_name, output_type, content)
# format: {"interface_name": XXX, "output_type": XXX, "content": XXX}
(last_output_json_obj['interface_name'],
last_output_json_obj['output_type'],
last_output_json_obj['content']) = output_history[i]
# send
messages2send.append(dumps(last_output_json_obj))
# send result if have
for message2send in messages2send:
print(f"send: {message} -> {message2send}")
await websocket.send(message2send)
# refresh output length
len_outputs = len(output_history)


if __name__ == '__main__':
# Config
d_host = '127.0.0.1'
host = input(f'Please input host (default {d_host}): ')
host = host if len(host) > 0 else d_host

d_port = 8765
port = input(f'Please input port (default {d_port}): ')
port = int(port) if port else d_port

# Launch
asyncio.get_event_loop().run_until_complete(
websockets.serve(handler, host, port)
)
print(f'WS server launched on {host}:{port}.')
asyncio.get_event_loop().run_forever()
21 changes: 11 additions & 10 deletions pynars/Interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ def narsese_parse_safe(narsese: str) -> Union[None, Task]:

class NARSOutput:
type: PrintType
content: any
content: str
p: float
d: float
q: float
comment_title: str
end: str
Expand Down Expand Up @@ -214,7 +215,7 @@ def reasoner(self) -> Reasoner:
# NARS constructor & initialization #

@staticmethod
def construct_interface(seed=-1, memory=100, capacity=100, silent: bool = False):
def construct_interface(seed:int=-1, memory:int=100, capacity:int=100, silent: bool = False):
'''Construct the reasoner using specific construction parameters instead of having to construct the reasoner itself in each constructor'''
return NARSInterface(seed=seed,
NARS=Reasoner(
Expand Down Expand Up @@ -263,22 +264,22 @@ def print_output(self, type: PrintType, content: any, p: float = None, d: float
end=end)

# _event_handlers: List[function] = [] # ! error: name 'function' is not defined
_event_handlers: list = []
_output_handlers: list = []

@property # read only
def event_handlers(self):
def output_handlers(self):
'''Registry of event handlers
Standard format:
```
def handler(out: NARSOutput):
# your code
```
'''
return self._event_handlers
return self._output_handlers

def _handle_NARS_output(self, out: NARSOutput):
def _handle_NARS_output(self, out: NARSOutput) -> None:
'''Internally traverses the event handler registry table, running its internal functions one by one'''
for handler in self._event_handlers:
for handler in self._output_handlers:
try:
handler(out)
except BaseException as e:
Expand Down Expand Up @@ -308,7 +309,7 @@ def execute_file(self, path: Union[Path, str]) -> None:
_input_history: List[str] = []

@property # readonly
def input_history(self):
def input_history(self) -> List[str]:
'''Records texts (statements) entered into the interface'''
return self._input_history

Expand Down Expand Up @@ -410,7 +411,7 @@ def _handle_lines(self, lines: str) -> List[NARSOutput]:
return outs

# run line
def run_line(self, reasoner: reasoner, line: str):
def run_line(self, reasoner: reasoner, line: str) -> Union[None, List[Task]]:
'''Run one line of input'''
line = line.strip(' \r\n\t') # ignore spaces
# special notations
Expand All @@ -426,7 +427,7 @@ def run_line(self, reasoner: reasoner, line: str):
self.print_output(
PrintType.ERROR, f'parse "{line}" failed!'
)
return
return None
# empty or comments
elif len(line) == 0 or line.startswith("//") or line.startswith("'"):
return None
Expand Down

0 comments on commit 299cab9

Please sign in to comment.