Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
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
15 changes: 7 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
OPENAI_API_KEY=""
DB_HOST = "db"
DB_NAME = "kodee-demo"
DB_USERNAME = "kodee-demo"
DB_PASSWORD = "password"
REDIS_HOST = "redis"
REDIS_PORT = "6379"
REDIS_PASSWORD = "password"
DB_HOST=db # Docker Compose Postgres service name
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_NAME=kodee
REDIS_HOST=redis # Docker Compose Redis service name
REDIS_PASSWORD=localpass
OPENAI_API_KEY=sk-xxx # Your OpenAI key if needed
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ FROM python:3.11
COPY requirements.txt /requirements.txt
RUN pip install --no-cache-dir -r /requirements.txt
COPY .env .env
COPY ./app /app
WORKDIR /app/app/
COPY ./app /app/app
WORKDIR /app
ENV PYTHONPATH=/app

CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0"]
26 changes: 12 additions & 14 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
setup: first_run create_venv install_alembic run_migrations
# Makefile for Docker-native development workflow
# Use these commands for all regular development tasks

# Start all services using Docker Compose and build if needed
up:
docker-compose -f docker-compose.yml up --build
docker compose up --build -d

first_run:
docker-compose -f docker-compose.yml up --build -d
# Stop and remove all services and persistent volumes
# down:
# docker compose down -v

create_venv:
python3 -m venv .venv
@echo "Virtual environment created."
# Run Alembic database migrations inside Docker
migrate:
docker compose exec web alembic upgrade head

install_alembic:
. .venv/bin/activate; pip install alembic psycopg2-binary python-dotenv
@echo "Alembic and psycopg2 installed in the virtual environment."

run_migrations:
. .venv/bin/activate; sleep 5; export DB_HOST=localhost; alembic upgrade head
@echo "Alembic migrations applied."
# Setup = up + migrate, for convenience
setup: up migrate
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,31 @@ post: [How we built one of the most advanced LLM-based chat assistants: Lessons
This command starts all the necessary services. Your **Kodee-demo** environment should now be up and running, ready for
use.

## Quickstart: Docker Compose

1. Clone the repo and copy `.env.example` to `.env`.
2. Start the stack and auto-run migrations:

```bash
docker compose up --build -d
make migrate # Or: docker compose exec web alembic upgrade head
```

- You can start/stop containers with `make up` and `make down`.
- Environment variables are managed in `.env`—see `.env.example` for required keys.

### Environment Variables Example

```
DB_HOST=db # Docker Compose Postgres service name
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_NAME=kodee
REDIS_HOST=redis # Docker Compose Redis service name
REDIS_PASSWORD=localpass
OPENAI_API_KEY=sk-xxx # Your OpenAI key if needed
```

## API endpoints

### Initialize chat session
Expand Down
Empty file added app/__init__.py
Empty file.
Empty file added app/api/__init__.py
Empty file.
Empty file added app/api/endpoints/__init__.py
Empty file.
15 changes: 8 additions & 7 deletions app/api/endpoints/chat.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from fastapi import APIRouter
from models.chat.chat_initialization_input_model import ChatInitializationInputModel
from models.chat.chat_message_input_model import ChatMessage
from models.chat.chat_message_output_model import ConversationMessagesOutput
from models.chat.chat_restart_input_model import ChatRestartInputModel
from services.chat_services.chat_respond import chat_service
from services.chat_services.chat_initialization import chat_initialization_service
from services.chat_services.chat_restart import restart_conversation_service

from app.models.chat.chat_initialization_input_model import ChatInitializationInputModel
from app.models.chat.chat_message_input_model import ChatMessage
from app.models.chat.chat_message_output_model import ConversationMessagesOutput
from app.models.chat.chat_restart_input_model import ChatRestartInputModel
from app.services.chat_services.chat_initialization import chat_initialization_service
from app.services.chat_services.chat_respond import chat_service
from app.services.chat_services.chat_restart import restart_conversation_service

chat_router = APIRouter()

Expand Down
5 changes: 3 additions & 2 deletions app/api/endpoints/conversation_history.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from fastapi import APIRouter, Query
from services.history.history_events import history_events_service
from services.history.history_messages import history_messages_service

from app.services.history.history_events import history_events_service
from app.services.history.history_messages import history_messages_service

history_router = APIRouter()

Expand Down
Empty file added app/api/external/__init__.py
Empty file.
Empty file.
4 changes: 3 additions & 1 deletion app/api/external/gpt_clients/cost_calculation_helpers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
def calculate_openai_cost(model, prompt_tokens, completion_tokens) -> float | str:
if model in openai_pricing_per_1k_tokens:
pricing = openai_pricing_per_1k_tokens[model]
total_cost = (prompt_tokens * pricing["prompt"] / 1000) + (completion_tokens * pricing["completion"] / 1000)
total_cost = (prompt_tokens * pricing["prompt"] / 1000) + (
completion_tokens * pricing["completion"] / 1000
)
return total_cost
else:
return "Model not found"
Expand Down
Empty file.
99 changes: 55 additions & 44 deletions app/api/external/gpt_clients/openai/openai_client.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
from typing import List, Dict, Any, Optional
import logging
from time import time
from typing import Any, Dict, List, Optional

from openai import AsyncOpenAI
from openai.types.chat import ChatCompletion
from api.external.gpt_clients.cost_calculation_helpers import calculate_openai_cost
from api.external.gpt_clients.gpt_enums import (
GPTResponseFormat,

from app.api.external.gpt_clients.cost_calculation_helpers import calculate_openai_cost
from app.api.external.gpt_clients.gpt_enums import (
GPTActionNames,
GPTChatbotNames,
GPTResponseFormat,
GPTTeamNames,
GPTActionNames,
GPTTemperature,
)
from api.external.gpt_clients.openai.openai_enums import OpenAIModel
from helpers.conversation import filter_out_system_messages
from helpers.gpt_helper import return_temperature_float_value
from helpers.tenacity_retry_strategies import openai_retry_strategy
from utils.logger.logger import Logger
from utils.env_constants import OPENAI_API_KEY
from time import time
import logging
from app.api.external.gpt_clients.openai.openai_enums import OpenAIModel
from app.helpers.conversation import filter_out_system_messages
from app.helpers.gpt_helper import return_temperature_float_value
from app.helpers.tenacity_retry_strategies import openai_retry_strategy
from app.utils.env_constants import OPENAI_API_KEY
from app.utils.logger.logger import Logger

TIMEOUT_SECONDS = 45
DEFAULT_MAX_TOKENS = 2048
Expand All @@ -28,16 +30,24 @@
class OpenAIChat:
@openai_retry_strategy
async def get_response(
self,
messages: List[dict],
model: OpenAIModel,
action_name: GPTActionNames,
team_name: GPTTeamNames,
chatbot_name: Optional[GPTChatbotNames] = None,
response_format: GPTResponseFormat = GPTResponseFormat.TEXT,
temperature: GPTTemperature = GPTTemperature.POINT_FIVE,
max_tokens: int = DEFAULT_MAX_TOKENS,
self,
messages: List[dict],
model: OpenAIModel,
action_name: GPTActionNames,
team_name: GPTTeamNames,
chatbot_name: Optional[GPTChatbotNames] = None,
response_format: GPTResponseFormat = GPTResponseFormat.TEXT,
temperature: GPTTemperature = GPTTemperature.POINT_FIVE,
max_tokens: int = DEFAULT_MAX_TOKENS,
) -> ChatCompletion | None:
logger.log(
"OpenAIChat.get_response called",
level=logging.INFO,
model=model,
action_name=action_name,
team_name=team_name,
chatbot_name=chatbot_name,
)
try:
start_time = time()

Expand All @@ -51,7 +61,9 @@ async def get_response(
)

process_time = time() - start_time
total_cost = calculate_openai_cost(model, response.usage.prompt_tokens, response.usage.completion_tokens)
total_cost = calculate_openai_cost(
model, response.usage.prompt_tokens, response.usage.completion_tokens
)

logger.log(
"OpenAI Token usage",
Expand All @@ -62,40 +74,35 @@ async def get_response(
prompt_tokens=response.usage.prompt_tokens,
completion_tokens=response.usage.completion_tokens,
total_tokens=response.usage.total_tokens,
requests=self.get_response.retry.statistics["attempt_number"],
requests=self.get_response.retry.statistics.get("attempt_number"),
response_time=process_time,
cost=total_cost,
)

return response
except Exception as e:
logger.log(
"GPT Exception occurred",
level=logging.WARNING,
service="OpenAI",
payload=str(e),
model_name=model,
f"Exception in get_response: {e}",
level=logging.ERROR,
model=model,
action_name=action_name,
method_name="get_response",
team_name=team_name,
chatbot_name=chatbot_name,
exception_type=type(e).__name__,
conversation_messages=filter_out_system_messages(messages),
)
raise

@openai_retry_strategy
async def get_response_with_tools(
self,
messages: List[dict],
action_name: GPTActionNames,
team_name: GPTTeamNames,
chatbot_name: GPTChatbotNames,
tools: List[Dict[str, Any]],
model: OpenAIModel,
response_format: GPTResponseFormat = GPTResponseFormat.TEXT,
temperature: GPTTemperature = GPTTemperature.POINT_FIVE,
max_tokens: int = DEFAULT_MAX_TOKENS,
self,
messages: List[dict],
action_name: GPTActionNames,
team_name: GPTTeamNames,
chatbot_name: GPTChatbotNames,
tools: List[Dict[str, Any]],
model: OpenAIModel,
response_format: GPTResponseFormat = GPTResponseFormat.TEXT,
temperature: GPTTemperature = GPTTemperature.POINT_FIVE,
max_tokens: int = DEFAULT_MAX_TOKENS,
) -> ChatCompletion | None:
try:
start_time = time()
Expand All @@ -112,7 +119,9 @@ async def get_response_with_tools(
)

process_time = time() - start_time
total_cost = calculate_openai_cost(model, response.usage.prompt_tokens, response.usage.completion_tokens)
total_cost = calculate_openai_cost(
model, response.usage.prompt_tokens, response.usage.completion_tokens
)
logger.log(
"OpenAI With Tools Token usage",
team_name=team_name,
Expand All @@ -122,7 +131,9 @@ async def get_response_with_tools(
prompt_tokens=response.usage.prompt_tokens,
completion_tokens=response.usage.completion_tokens,
total_tokens=response.usage.total_tokens,
requests=self.get_response_with_tools.retry.statistics.get("attempt_number"),
requests=self.get_response_with_tools.retry.statistics.get(
"attempt_number"
),
response_time=process_time,
cost=total_cost,
)
Expand Down
Empty file added app/database/__init__.py
Empty file.
Loading