Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A new websocket-based ability to enable PyNARS communicate with Web client #36

Merged
merged 4 commits into from
Oct 22, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: 🚩 websocket server-based console ConsoleWeb
- 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
commit 299cab9e7ca8c634ccfca68aedfae353ba80bc22
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