Skip to content

Commit

Permalink
Removes assumption of node_engine NOT being installed as a package in… (
Browse files Browse the repository at this point in the history
#7)

* Removes assumption of node_engine NOT being installed as a package in module loader.

* Fixes README formatting errors.

* Removes relative path module component loading (for simplicity).

* Removes relative registry loading/merging (for simplicity as an imported package).
  • Loading branch information
payneio committed Mar 20, 2024
1 parent 81a03b2 commit 6a9d274
Show file tree
Hide file tree
Showing 3 changed files with 25 additions and 123 deletions.
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Node Engine is designed for rapid experimentation and development of new compone

Why not [Semantic Kernel | Promptflow | Autogen | LangChain | etc.]? This framework has been built from the ground up specifically for the experimentation use-case for our team and next areas of investment. Each of those frameworks have their own idiosyncrasies that work well for their use-cases, but don't allow us to run as fast as we can with this approach and the ability for us to quickly extend it to meet our not-yet-discovered needs. Learnings from this framework will be used to inform future work in Semantic Kernel and other areas.

# Concepts
## Concepts

Detailed [documentation](./docs/node_engine/README.md) of Node Engine and how it works is provided.

Expand Down Expand Up @@ -56,15 +56,15 @@ When state must be provided to a component, there are a few recommended approach

It is recommended to avoid storing flow-level state in the component config, as it is designed to be used for configuration and not state. Instead, update the context with the state and it will be passed to the remaining components in the flow.

# Quickstart
## Quickstart

## Prerequisites
### Prerequisites

The steps below assume your dev environment has Python 3.10.11+ or 3.11+ installed and available in the console. You can execute this command to check your version: `python3 --version`.

It's highly recommended to develop with Node Engine using python virtual environments, with all the benefits that come. See https://docs.python.org/3/library/venv.html for details.

## Install Node Engine and dependencies
### Install Node Engine and dependencies

These steps will set up all of the prerequisites for running the Node Engine
service locally, along with anything needed for the example scripts and notebooks.
Expand Down Expand Up @@ -103,7 +103,7 @@ you can skip to the next section and start Node Engine service.

- Edit `.env` as needed, adding your credentials.

## Start Node Engine service
### Start Node Engine service

Unless Node Engine service is already running, open a terminal console and run
the following commands from the root of the project:
Expand All @@ -119,12 +119,12 @@ the following commands from the root of the project:
- Start local Node Engine service. The service defaults to port 8000.

# alternative with VS Code debugger:
# Shift+Ctrl+D and choose 'Node Engine Service'
# Shift+Ctrl+D and choose 'Node Engine Service'
node-engine-service --registry-root examples

# Quick Tests
## Quick Tests

## #1 Run sample flow from command line
### #1 Run sample flow from command line

- After starting the service, open a new console.
- Activate the virtual environment:
Expand All @@ -141,7 +141,7 @@ the following commands from the root of the project:

- You should see a sample JSON output on screen, without errors, and a context containing "Hello World".

## #2 Test SSE (server side events) via Postman
### #2 Test SSE (server side events) via Postman

- Launch Postman and create a new GET request with the following url:
- `http://127.0.0.1:8000/sse?session_id=my-session-12345&connection_id=my-connection-12345`
Expand All @@ -153,12 +153,12 @@ the following commands from the root of the project:

![Alt text](docs/quickstart.png)

# Next steps
## Next steps

Once the service is running, you can run the example [scripts](../examples/scripts/)
and [notebooks](../examples/notebooks/).

## Invoke a flow from command line
### Invoke a flow from command line

This [invoke-flow.py](examples/scripts/invoke-flow.py) script loads a flow definition and send it to the service to be executed. The examples/definitions folder contains some examples.

Expand All @@ -169,11 +169,11 @@ Example:

python3 examples/scripts/invoke-flow.py examples/definitions/simple-cognition.json --session-id sid123 --log-level debug --stream-log

## Simple Chat client
### Simple Chat client

The examples folder contain a sample chat app with a custom UI to chat with OpenAI models, see the [documentation here](examples/apps/simple-chat-client/README.md) to test it. In this case you won't need to start the service because the app starts the service automatically.

## Watch service logs
### Watch service logs

This [debug-service.py](examples/scripts/debug-service.py) script attaches to the service and emits log events for a specific session.

Expand All @@ -187,7 +187,7 @@ Example:

- Interact with the chat app and observe logs coming through in the console.

## Develop your apps
### Develop your apps

To experiment with developing new apps, scripts, flows and components, see the
[Development guide](docs/DEVELOPMENT.md) for more details.
68 changes: 1 addition & 67 deletions node_engine/libs/component_loaders/module_component_loader.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
# Copyright (c) Microsoft. All rights reserved.

import importlib
import os
import pathlib
import traceback

from node_engine.libs.component_loaders.component_loader import ComponentLoader
from node_engine.libs.node_engine_component import NodeEngineComponent
from node_engine.models.flow_definition import FlowDefinition
from node_engine.models.flow_executor import FlowExecutor

# get path for parent of node_engine library
root_path = str(pathlib.Path(__file__).parent.parent.parent.parent.absolute())


class ModuleComponentLoader:
@staticmethod
Expand All @@ -21,62 +15,10 @@ def load(
component_key: str,
module_name: str,
class_name: str,
registry_root: str,
executor: FlowExecutor,
tunnel_authorization: str | None = None,
) -> NodeEngineComponent:
try:
module = importlib.import_module(module_name)

return ComponentLoader.load(
flow_definition,
component_key,
module,
class_name,
executor,
tunnel_authorization,
)
except ModuleNotFoundError as e:
if e.name != module_name:
stacktrace = traceback.format_exc()
raise Exception(
f"Error importing module '{module_name}': {e}. {stacktrace}"
)

pass

package = None

# check if module exists in local files or any parent directories
# until root directory is reached
current_path = registry_root
while True:
# check if module exists
if os.path.isfile(
os.path.join(current_path, "components", f"{module_name}.py")
):
# get relative path
relative_path = pathlib.Path(current_path).relative_to(
pathlib.Path(root_path)
)
# convert to dot notation
package = ".".join(relative_path.parts + ("components",))
break
# check if at root
if len(current_path) <= len(root_path):
break
# go up one level
current_path = os.path.dirname(current_path)

try:
name = f".{module_name}"
module = importlib.import_module(name, package)
except Exception as exception:
stacktrace = traceback.format_exc()
raise Exception(
f"Error importing module '{module_name}': {exception}. {stacktrace}"
)

module = importlib.import_module(module_name)
return ComponentLoader.load(
flow_definition,
component_key,
Expand All @@ -85,11 +27,3 @@ def load(
executor,
tunnel_authorization,
)

@staticmethod
def path_to_package(path: str) -> str:
# get relative path
relative_path = pathlib.Path(path).relative_to(pathlib.Path(root_path))
# convert to dot notation
package = ".".join(relative_path.parts + ("components",))
return package
52 changes: 10 additions & 42 deletions node_engine/libs/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@

import json
import os
import pathlib

from dotenv import load_dotenv

from node_engine.libs.component_loaders.code_component_loader import CodeComponentLoader
from node_engine.libs.component_loaders.endpoint_component_loader import (
Expand All @@ -18,16 +15,12 @@
from node_engine.models.flow_definition import FlowDefinition
from node_engine.models.flow_executor import FlowExecutor

load_dotenv()

registry_file_name = "registry.json"

# get path for parent of node_engine library
root_path = str(pathlib.Path(__file__).parent.parent.parent.absolute())


class Registry:
def __init__(self, root_path: str) -> None:
# We assume the registry file will be found at <root_path>/registry.json.
self.root_path = root_path

# Load each time needed so that changes to the registry file are reflected
Expand All @@ -41,43 +34,19 @@ def load_from_file(registry_file) -> list[ComponentRegistration]:

return component_definitions

# helper: merge two lists of components
def merge_component_definitions(
component_definitions: list[ComponentRegistration],
additional_component_definitions: list[ComponentRegistration],
) -> list[ComponentRegistration]:
for additional_component in additional_component_definitions:
if additional_component.key not in [
component.key for component in component_definitions
]:
component_definitions.append(additional_component)
return component_definitions
# Load components from registry file.
registry_file_path = os.path.join(self.root_path, registry_file_name)
if not os.path.isfile(registry_file_path):
return []

component_definitions = []

# start in local registry and check if registry file exists, otherwise walk
# up parent directories until root directory is reached
current_path = self.root_path
while True:
# check if registry file exists
current_path_file = os.path.join(current_path, registry_file_name)
if os.path.isfile(current_path_file):
additional_component_definitions = load_from_file(current_path_file)
component_definitions = merge_component_definitions(
component_definitions, additional_component_definitions
)
# check if at root or below
if len(current_path) <= len(root_path):
break
# go up one level
current_path = os.path.dirname(current_path)
component_definitions = load_from_file(registry_file_path)

# sort components by key
# Sort components by key.
sorted_component_definitions = sorted(
component_definitions, key=lambda component: component.key
)

# return sorted components
# Return sorted components.
return sorted_component_definitions

def load_component(
Expand All @@ -88,7 +57,7 @@ def load_component(
executor: FlowExecutor,
tunnel_authorization: str | None = None,
) -> NodeEngineComponent | None:
# get the component registration for the given key
# Get the component registration for the given key.
component_registration = (
next(
(
Expand All @@ -104,7 +73,7 @@ def load_component(
if component_registration is None:
return None

# load the component
# Load the component.
match component_registration.type:
case "endpoint":
component = EndpointComponentLoader.load(
Expand All @@ -121,7 +90,6 @@ def load_component(
component_key,
component_registration.config["module"],
component_registration.config["class"],
self.root_path,
executor=executor,
tunnel_authorization=tunnel_authorization,
)
Expand Down

0 comments on commit 6a9d274

Please sign in to comment.