Skip to content

Commit

Permalink
Merge pull request #6 from yonishelach/make-controller
Browse files Browse the repository at this point in the history
Make controller
  • Loading branch information
guy1992l authored Jul 21, 2024
2 parents 7bd7b7c + bed1bef commit 22a894d
Show file tree
Hide file tree
Showing 11 changed files with 527 additions and 37 deletions.
26 changes: 26 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright 2023 Iguazio
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

CONTROLLER_NAME = "genai-factory-controller"

.PHONY: controller
controller:
# Build controller's image:
docker build -f controller/Dockerfile -t $(CONTROLLER_NAME):latest .

# Run controller locally in a container:
docker run -d --net host --name $(CONTROLLER_NAME) $(CONTROLLER_NAME):latest

# Announce the server is running:
@echo "GenAI Factory Controller is running in the background"
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# GenAI Factory

Demo an end to end LLM agent solution with modular architecture, persistent storage and front-end UI that can work with various LLM models and storage solutions.

the configuration is specified in a YAML file, which indicate the model, embeddings, storage to use, and various parameters.
the user can point to the configuration file by setting the `AGENT_CONFIG_PATH` environment variable.

environment variables and credentials can be loaded from a `.env` file in the root directory. or an alternate path set by the `AGENT_ENV_PATH` environment variable.
data can be stored in local files or remote SQL and Vector databases. the local file storage path can be set by the `AGENT_DATA_PATH` environment variable (defaults to `./data/`).

# Getting it to work

## Deploy the controller
This command will start the API controller server into a local docker container.
```shell
make controller
```

## Initialize the database:
The database is Initialized when building the controller.
In order to erase and start fresh, we can simply use the controller's command line interface.

```shell
python -m controller.src.main initdb
```

## To start the application's API:

```shell
uvicorn pipeline:app
```

## To start UI:
Future work will include a UI command to run the UI.
```shell
make ui
```

# CLI usage

To ingest data into the vector database:
```shell
python -m controller.src.main ingest -l web https://milvus.io/docs/overview.md
```

To ask a question:
```shell
python -m controller.src.main query "What is a vector?"
```


Full CLI:

```shell
python -m controller.src.main

Usage: python -m controller.src.main [OPTIONS] COMMAND [ARGS]...

Options:
--help Show this message and exit.

Commands:
config Print the config as a yaml file
ingest Ingest documents into the vector database
initdb Initialize the database (delete old tables)
list List the different objects in the database (by category)
query Run a chat query on the vector database collection
```
45 changes: 45 additions & 0 deletions controller/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright 2023 Iguazio
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

ARG PYTHON_VERSION=3.9.19
FROM python:${PYTHON_VERSION}

# Update OS packages:
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get -y upgrade && \
rm -rf /var/lib/apt/lists/*

RUN apt update -qqq \
&& apt -y upgrade \
&& apt install -y \
build-essential \
cmake \
gcc \
&& rm -rf /var/lib/apt/lists/* \

WORKDIR /controller
COPY /controller/src /controller/src
COPY /controller/requirements.txt /controller/

# Make the data directory:
RUN mkdir -p ../data

# Install requirements:
RUN pip install -r /controller/requirements.txt

# Initiate database:
RUN python -m controller.src.main initdb

# Run the controller's API server:
CMD ["uvicorn", "controller.src.api:app", "--port", "8001"]
4 changes: 3 additions & 1 deletion server/requirements.txt → controller/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ fastapi==0.85.1
SQLAlchemy~=2.0.23
uvicorn
python-dotenv
pyyaml
pyyaml
requests
tabulate
File renamed without changes.
112 changes: 81 additions & 31 deletions server/src/api.py → controller/src/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,18 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import json
from typing import List, Optional, Tuple, Union

import requests
from fastapi import (APIRouter, Depends, FastAPI, File, Header, Request,
UploadFile)
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

from .config import logger
from .model import ChatSession, DocCollection, OutputMode, QueryItem, User
from .sqlclient import client
from controller.src.config import config
from controller.src.model import ChatSession, DocCollection, OutputMode, User
from controller.src.sqlclient import client

app = FastAPI()

Expand Down Expand Up @@ -56,44 +57,93 @@ class AuthInfo(BaseModel):


# placeholder for extracting the Auth info from the request
async def get_auth_user(
def get_auth_user(
request: Request, x_username: Union[str, None] = Header(None)
) -> AuthInfo:
"""Get the user from the database"""
token = request.cookies.get("Authorization", "")
if x_username:
return AuthInfo(username=x_username, token=token)
else:
return AuthInfo(username="[email protected]", token=token)
return AuthInfo(username="[email protected]", token=token)


def _send_to_application(path: str, method: str = "POST", request=None, auth=None, **kwargs):
"""
Send a request to the application's API.
:param path: The API path to send the request to.
:param method: The HTTP method to use: GET, POST, PUT, DELETE, etc.
:param request: The FastAPI request object. If provided, the data will be taken from the body of the request.
:param auth: The authentication information to use. If provided, the username will be added to the headers.
:param kwargs: Additional keyword arguments to pass in the request function. For example, headers, params, etc.
:return: The JSON response from the application.
"""
url = f"{config.application_url}/api/{path}"

if isinstance(request, Request):
# If the request is a FastAPI request, get the data from the body
kwargs["data"] = request._body.decode("utf-8")
if auth is not None:
kwargs["headers"] = {"x_username": auth.username}

response = requests.request(
method=method,
url=url,
**kwargs,
)

# Check the response
if response.status_code == 200:
# If the request was successful, return the JSON response
return response.json()
else:
# If the request failed, raise an exception
response.raise_for_status()


@router.post("/tables")
async def create_tables(drop_old: bool = False, names: list[str] = None):
def create_tables(drop_old: bool = False, names: list[str] = None):
return client.create_tables(drop_old=drop_old, names=names)


@router.post("/pipeline/{name}/run")
async def run_pipeline(
request: Request, name: str, item: QueryItem, auth=Depends(get_auth_user)
def run_pipeline(
request: Request, name: str, auth=Depends(get_auth_user)
):
"""This is the query command"""
app_server = request.app.extra.get("app_server")
if not app_server:
raise ValueError("app_server not found in app")
event = {
"username": auth.username,
"session_id": item.session_id,
"query": item.question,
"collection_name": item.collection,
return _send_to_application(
path=f"pipeline/{name}/run",
method="POST",
request=request,
auth=auth,
)


@router.post("/collections/{collection}/{loader}/ingest")
def ingest(
collection, path, loader, metadata, version, from_file, auth=Depends(get_auth_user)
):
"""Ingest documents into the vector database"""
params = {
"path": path,
"from_file": from_file,
"version": version,
}
logger.debug(f"running pipeline {name}: {event}")
resp = app_server.run_pipeline(name, event)
print(f"resp: {resp}")
return resp
if metadata is not None:
params["metadata"] = json.dumps(metadata)

return _send_to_application(
path=f"collections/{collection}/{loader}/ingest",
method="POST",
params=params,
auth=auth,
)


@router.get("/collections")
async def list_collections(
def list_collections(
owner: str = None,
labels: Optional[List[Tuple[str, str]]] = None,
mode: OutputMode = OutputMode.Details,
Expand All @@ -105,12 +155,12 @@ async def list_collections(


@router.get("/collection/{name}")
async def get_collection(name: str, session=Depends(get_db)):
def get_collection(name: str, session=Depends(get_db)):
return client.get_collection(name, session=session)


@router.post("/collection/{name}")
async def create_collection(
def create_collection(
request: Request,
name: str,
collection: DocCollection,
Expand All @@ -122,7 +172,7 @@ async def create_collection(


@router.get("/users")
async def list_users(
def list_users(
email: str = None,
username: str = None,
mode: OutputMode = OutputMode.Details,
Expand All @@ -134,12 +184,12 @@ async def list_users(


@router.get("/user/{username}")
async def get_user(username: str, session=Depends(get_db)):
def get_user(username: str, session=Depends(get_db)):
return client.get_user(username, session=session)


@router.post("/user/{username}")
async def create_user(
def create_user(
user: User,
username: str,
session=Depends(get_db),
Expand All @@ -149,13 +199,13 @@ async def create_user(


@router.delete("/user/{username}")
async def delete_user(username: str, session=Depends(get_db)):
def delete_user(username: str, session=Depends(get_db)):
return client.delete_user(username, session=session)


# get last user sessions, specify user and last
@router.get("/user/{username}/sessions")
async def list_user_sessions(
def list_user_sessions(
username: str,
last: int = 0,
created: str = None,
Expand All @@ -168,7 +218,7 @@ async def list_user_sessions(


@router.put("/user/{username}")
async def update_user(
def update_user(
user: User,
username: str,
session=Depends(get_db),
Expand All @@ -178,7 +228,7 @@ async def update_user(

# add routs for chat sessions, list_sessions, get_session
@router.post("/session")
async def create_session(chat_session: ChatSession, session=Depends(get_db)):
def create_session(chat_session: ChatSession, session=Depends(get_db)):
return client.create_session(chat_session, session=session)


Expand Down
1 change: 1 addition & 0 deletions server/src/config.py → controller/src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class CtrlConfig(BaseModel):
log_level: str = "DEBUG"
# SQL Database
sql_connection_str: str = default_db_path
application_url: str = "http://localhost:8000"

def print(self):
print(yaml.dump(self.dict()))
Expand Down
Loading

0 comments on commit 22a894d

Please sign in to comment.