Welcome! This README will explain the general structure of the CHIP Modular System, and how to use it appropriately for your goals.
- Make sure bash is installed on the system (consider using WSL2 on Windows).
- The system uses Docker Compose, make sure it is installed and functioning. Docker Desktop is recommended for good overview, using the WSL2 backend is also highly recommended.
On Mac, remove the line containing
"credsStore"
from~/.docker/config.json
. - A system that has a CPU with virtualization support.
- [OPTIONAL]: GPU with CUDA support, for running LLMs locally.
For a quick start with default settings, just navigate to the root folder and use ./chip.sh start
. You may access the front end at http://localhost:9000
.
The system works with the notion of "core" modules, and "non-core" modules. There are five different types of core modules:
- Front End
- Triple Extractor
- Reasoner
- Response Generator
- Logger
Any module that doesn't classify as one of these module types, is considered not to be a core module. All core modules must expose a /process
route, which they use to transfer JSON data to one another via POST requests. A description of the expected models for the bodies of the requests will come after this subsection.
The general communication structure is as follows:
Logger
/|\
________________________|_________________________
| | | |
Front End ==> Triple Extractor ==> Reasoner ==> Response Generator ==|
/|\ |
|-------------------------SSE-----------------------------------|
All core modules communicate with the logger for logging, but other than that the communication is a pre-determined chain. At the end, a Server-Sent-Event (SSE) is used to communicate back to the Front End that the system has finished processing and has a chat response ready.
Core modules may communicate with other non-core/side modules to query e.g. knowledge, or to cache things via Redis, but this is the core loop that can always assumed to be present.
These are the models that the core modules expect the JSON bodies to conform to, sent via a POST request to the .../process
route of the next module in the chain.
[POST] /process
:
{
"patient_name": <string>, // The name of the user currently chatting
"sentence": <string>, // The sentence that the user submitted
"timestamp": <string> // The time at which the user submitted the sentence (ISO format)
}
[POST] /process
:
{
"sentence_data": {
"patient_name": <string>, // The name of the user currently chatting
"sentence": <string>, // The sentence that the user submitted
"timestamp": <string> // The time at which the user submitted the sentence (ISO format)
},
"triples": [
{
"subject":<string>,
"object": <string>,
"predicate":<string>
},
...
]
}
[POST] /process
:
{
"sentence_data": {
"patient_name": <string>, // The name of the user currently chatting.
"sentence": <string>, // The sentence that the user submitted.
"timestamp": <string> // The time at which the user submitted the sentence (ISO format).
},
"type": <string: Q|A>, // Whether the reasoner decided to give an answer (A) or to request more information (Q).
"data": <dict> // A dict containing the output of the reasoner.
}
[POST] /process
:
{
"message": <string> // The generated message.
}
The Logger module is special, in that its format is already pre-determined by Python's logging framework.
The system has a set of pre-configured core modules that it will start up with, specified in core-modules.yaml
. This file initially does not exist, but it will be created (from default values) along with other configuration files by running the chip.sh
script without any subcommands, and it looks like this:
logger_module: logger-default
frontend_module: front-end-quasar
response_generator_module: response-generator-gemini
reasoner_module: reasoning-demo
triple_extractor_module: text-to-triples-rule-based
The module names correspond to the name of the directory they reside in within modules
directory.
A second configuration file that will be generated is setup.env
. This environment file contains the url/port mappings for the modules, which are derived from their respective compose files. This is how the modules know how to reach each other. The mapping uses the following convention: MODULE_NAME_CONVERTED_TO_CAPS_AND_UNDERSCORES=<module-name>:<module-port>
.
Finally, every module also has its own config.env
which will be copied from the default values in their config.env.default
file. This config.env
file is not tracked by git, so it is a good place to store things like API keys that modules may need. They will be available within the container as an environment variable.
Full command overview for the chip.sh
script:
-
chip.sh
(without args): generates thecore-modules.yaml
file if it doesn't exist fromcore-modules.yaml.default
, and then generatessetup.env
containing the module mappings.setup.env
will always be overwritten if it already exists. This also takes place for any of the subcommands. It also generatesconfig.env
for any module that didn't have it yet, from theirconfig.env.default
. Configs are not git-tracked, only their defaults are. -
chip.sh start [module name ...]
: builds and starts the system pre-configured with the current core modules specified incore-modules.yaml
and their dependencies, or starts specific modules and their dependencies given by name and separated by spaces. -
chip.sh build [module name ...]
: builds the core modules specified incore-modules.yaml
and their dependencies, or builds specific modules and their dependencies given by name and separated by spaces. -
chip.sh stop
: takes down any active modules. -
chip.sh clean
: removes all volume data, giving you a clean slate. -
chip.sh list
: prints a list of all available modules. -
chip.sh auto-complete
: adds auto-completion for the modules to the script. If you prefix this command withsource
, it will immediately load the auto-complete definitions in the current terminal, otherwise you have to restart the terminal for it to take effect.
For instance, if you are in the process of creating a new module, and just want to build it, you would use chip.sh build my-cool-new-module
. If you want to both build and start it, you would use chip.sh start my-cool-new-module
. Say your module has redis
and knowledge-demo
as dependencies, then docker-compose will automatically also build and start the redis
and knowledge-demo
modules for you.
All modules adhere to the following structure:
my-cool-new-module
|- Dockerfile (optional)
|- compose.yml
|- README.md
|- ...
The Dockerfile can be omitted, if compose.yml
specifies a particular image without further modifications. This may be the case for certain non-core modules, such as redis
.
How it practically works, is that all compose.yml
files of all modules will be collected by the chip.sh
script, and then merged into a big docker compose configuration using docker compose merge.
Here's an example of a minimal compose.yml
file of the my-cool-new-module
module, residing in a directory of the same name within the modules
directory:
services: # This is always present at the root.
my-cool-new-module: # SERVICE NAME MUST CORRESPOND TO DIRECTORY/MODULE NAME
env_file: setup.env # The module mappings, don't touch this.
expose:
- 5000 # The backend port, generally no need to touch this if using Flask.
build: ./modules/my-cool-new-module/. # Build according to the dockerfile in the current folder, only fix the module name.
volumes:
- ./modules/my-cool-new-module/app:/app # Bind mount, for syncing code changes, only fix the module name.
depends_on: ["redis"] # Modules that this module depends on and that will be started/built along with it.
Modules should generally use the Python Flask backend, which means that somewhere in the module's directory (often the root, but sometimes it is nested, e.g. see front-end-quasar
) there will be an app
directory, which is the Flask app. The Flask apps are always structured as follows:
app
|- tests... --> The tests
|- util... --> All custom utilities and code
|- routes.py --> All the routes and communication related code
|- __init__.py --> Flask initialization/setup
The idea is to keep a clean separation of concerns: functionality related code will only be found in util
, while routes.py
only concerns itself with route definitions and request sending.
The requirements.txt
file should generally reside next to the app
directory, which are both usually found in the root directory for most modules. Hence, the typical module looks like this:
my-cool-new-module
|- Dockerfile
|- compose.yml
|- README.md
|- app...
|- tests...
|- util...
|- routes.py
|- __init__.py
|- requirements.txt
The previous section should already have outlined most of the details regarding the module structure, but here is a quick guide to get you started right away.
-
The easiest way to get started is to just copy an existing module that most closely resembles what you want to create.
-
Then, think of a good name. The naming convention used for the core modules is as follows:
<MODULE_TYPE>-<NAME>
. If the module is non-core, then you can use any name, as long as it doesn't clash with the core module type names (e.g. the Redis module is just calledredis
). Say you want to make a new Reasoner, by the name "brain", then you call itreasoner-brain
. -
Rename everything that needs to be renamed:
- The module directory name
- The service name in the
compose.yml
file - Fix the paths for the volumes and the build directory in the
compose.yml
file.
-
Adjust/Add anything else you need in terms of configuration in the
compose.yml
file:- Did you expose all the ports you need?
- Are there dependencies such as
redis
? Did you add them? - Do you need additional bind mount or data volumes?
- Environment variables?
Check https://docs.docker.com/compose/ for detailed configuration help.
-
Tweak the Dockerfile:
- Change the base image, if you need something specific
- Install specific packages that you may need
- Install special requirements that you cannot put in
requirements.txt
, e.g. due to using a different package manager than pip for them. - Other custom/misc. system setup that you may need.
Check https://docs.docker.com/reference/dockerfile/ for a detailed Dockerfile reference.
You can build the module from the root folder using
./chip.sh build <MODULE_NAME>
. -
Tweak the Python code to your liking:
- Add your requirements to
requirements.txt
- Include your own code and utilities in
util
- Expose the routes you need in
routes.py
and call your code fromutil
- Write your own tests
- Change the module's appearance in the logs, by changing the service name in the
__init__.py
of theServiceNameFilter
If the bind mounts are setup properly in
compose.yml
, the code should automatically sync. If the module uses Flask, it will auto-reload the application whenever you make a change.You can run the module from the root folder using
./chip.sh start <MODULE_NAME>
. - Add your requirements to
-
Add any configuration you want to expose to
config.env.default
. They are made available as environment variables within the container, and the user may edit the generatedconfig.env
to tweak the settings you wish to make configurable. Think of things such as API keys, model name, performance settings, etc.
CI is setup for the project, and will run automatically for any module that has a tests
folder within an app
folder, which is generally the file structure that Flask adheres to. Modules that have no such folder will not be considered for the test runner.