Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
clemlesne committed Jan 20, 2024
1 parent 29a4188 commit e72e5da
Show file tree
Hide file tree
Showing 18 changed files with 94 additions and 74 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
.version.cache

# Temporary database
.local.sqlite
.local.sqlite*

# Local app config file
config.yaml
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ tunnel:
devtunnel host $(tunnel_name)

dev:
VERSION=$(version_full) EVENTS_DOMAIN=$(tunnel_url) python3 -m uvicorn main:api \
VERSION=$(version_full) API_EVENTS_DOMAIN=$(tunnel_url) python3 -m uvicorn main:api \
--header x-version:$${VERSION} \
--no-server-header \
--port 8080 \
Expand All @@ -64,7 +64,7 @@ start:
@echo "🛠️ Deploying to localhost..."
$(docker) run \
--detach \
--env EVENTS_DOMAIN=$(tunnel_url) \
--env API_EVENTS_DOMAIN=$(tunnel_url) \
--env VERSION=$(version_full) \
--mount type=bind,source="$(CURDIR)/.env",target="/app/.env" \
--mount type=bind,source="$(CURDIR)/config.yaml",target="/app/config.yaml" \
Expand Down
10 changes: 5 additions & 5 deletions bicep/app.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -72,27 +72,27 @@ resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
value: config
}
{
name: 'LOGGING_APP_LEVEL'
name: 'MONITORING_LOGGING_APP_LEVEL'
value: 'DEBUG'
}
{
name: 'EVENTS_DOMAIN'
name: 'API_EVENTS_DOMAIN'
value: appUrl
}
{
name: 'DATABASE_MODE'
value: 'cosmos_db'
}
{
name: 'COSMOS_ENDPOINT'
name: 'DATABASE_COSMOS_DB_ENDPOINT'
value: cosmos.properties.documentEndpoint
}
{
name: 'COSMOS_CONTAINER'
name: 'DATABASE_COSMOS_DB_CONTAINER'
value: container.name
}
{
name: 'COSMOS_DATABASE'
name: 'DATABASE_COSMOS_DB_DATABASE'
value: database.name
}
]
Expand Down
5 changes: 4 additions & 1 deletion helpers/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

# Load deps
from helpers.config_models.root import RootModel
from pydantic import ValidationError
import os
import yaml

Expand Down Expand Up @@ -34,6 +35,8 @@ class ConfigBadFormat(Exception):
try:
with open(path, "r", encoding="utf-8") as f:
CONFIG = RootModel.model_validate(yaml.safe_load(f))
except ValidationError as e:
raise ConfigBadFormat(f"Config values are not valid") from e
except Exception as e:
raise ConfigBadFormat(f'Config "{path}" is not valid YAML') from e
raise ConfigBadFormat(f"Config YAML format is not valid") from e
print(f'Config "{path}" loaded')
8 changes: 3 additions & 5 deletions helpers/config_models/api.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from os import environ
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings


class ApiModel(BaseModel):
events_domain: str = environ["EVENTS_DOMAIN"]
class ApiModel(BaseSettings, env_prefix="api_"):
events_domain: str
root_path: str = ""
version: str = Field(default=environ["VERSION"], frozen=True)
4 changes: 2 additions & 2 deletions helpers/config_models/cognitive_service.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from pydantic import BaseModel
from pydantic_settings import BaseSettings


class CognitiveServiceModel(BaseModel):
class CognitiveServiceModel(BaseSettings, env_prefix="cognitive_service_"):
endpoint: str
5 changes: 3 additions & 2 deletions helpers/config_models/communication_service.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from pydantic import BaseModel, SecretStr
from pydantic import SecretStr
from pydantic_extra_types.phone_numbers import PhoneNumber
from pydantic_settings import BaseSettings


# E164 is standard accross all Microsoft services
PhoneNumber.phone_format = "E164"


class CommunicationServiceModel(BaseModel):
class CommunicationServiceModel(BaseSettings, env_prefix="communication_service_"):
access_key: SecretStr
endpoint: str
phone_number: PhoneNumber
Expand Down
33 changes: 19 additions & 14 deletions helpers/config_models/database.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from enum import Enum
from os import environ
from pydantic import BaseModel, validator
from pydantic import validator
from pydantic_settings import BaseSettings
from typing import Optional


Expand All @@ -9,25 +9,30 @@ class Mode(str, Enum):
SQLITE = "sqlite"


class CosmosDbModel(BaseModel):
container: str = environ.get("COSMOS_CONTAINER", None) # type: ignore
database: str = environ.get("COSMOS_DATABASE", None) # type: ignore
endpoint: str = environ.get("COSMOS_ENDPOINT", None) # type: ignore
class CosmosDbModel(BaseSettings, env_prefix="database_cosmos_db_"):
container: str
database: str
endpoint: str


class SqliteModel(BaseModel):
class SqliteModel(BaseSettings, env_prefix="database_sqlite_"):
path: str = ".local.sqlite"
table: str = "calls"


class DatabaseModel(BaseModel):
class DatabaseModel(BaseSettings, env_prefix="database_"):
cosmos_db: Optional[CosmosDbModel] = None
mode: Mode = Mode(environ.get("DATABASE_MODE", Mode.SQLITE))
mode: Mode = Mode.SQLITE
sqlite: Optional[SqliteModel] = None

@validator("mode")
def check_mode(cls, v, values, **kwargs):
if v == Mode.COSMOS_DB and not values["cosmos_db"]:
@validator("cosmos_db", always=True)
def check_cosmos_db(cls, v, values, **kwargs):
if not v and values.get("mode", None) == Mode.COSMOS_DB:
raise ValueError("Cosmos DB config required")
elif v == Mode.SQLITE and not values["sqlite"]:
raise ValueError("SQLite config required")
return v

@validator("sqlite", always=True)
def check_sqlite(cls, v, values, **kwargs):
if not v and values.get("mode", None) == Mode.SQLITE:
raise ValueError("Sqlite config required")
return v
4 changes: 2 additions & 2 deletions helpers/config_models/eventgrid.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from pydantic import BaseModel
from pydantic_settings import BaseSettings


class EventgridModel(BaseModel):
class EventgridModel(BaseSettings, env_prefix="eventgrid_"):
resource_group: str
subscription_id: str
system_topic: str
15 changes: 5 additions & 10 deletions helpers/config_models/monitoring.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from enum import Enum
from os import environ
from pydantic import BaseModel
from pydantic_settings import BaseSettings


class LoggingLevel(str, Enum):
Expand All @@ -13,14 +12,10 @@ class LoggingLevel(str, Enum):
WARNING = "WARNING"


class LoggingMonitoringModel(BaseModel):
app_level: LoggingLevel = LoggingLevel(
environ.get("LOGGING_APP_LEVEL", LoggingLevel.INFO)
)
sys_level: LoggingLevel = LoggingLevel(
environ.get("LOGGING_SYS_LEVEL", LoggingLevel.WARNING)
)
class LoggingMonitoringModel(BaseSettings, env_prefix="monitoring_logging_"):
app_level: LoggingLevel = LoggingLevel.INFO
sys_level: LoggingLevel = LoggingLevel.WARNING


class MonitoringModel(BaseModel):
class MonitoringModel(BaseSettings, env_prefix="monitoring_"):
logging: LoggingMonitoringModel
5 changes: 3 additions & 2 deletions helpers/config_models/openai.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from pydantic import BaseModel, SecretStr
from pydantic import SecretStr
from pydantic_settings import BaseSettings
from typing import Optional


class OpenAiModel(BaseModel):
class OpenAiModel(BaseSettings, env_prefix="openai_"):
api_key: Optional[SecretStr] = None
endpoint: str
gpt_deployment: str
Expand Down
4 changes: 2 additions & 2 deletions helpers/config_models/resources.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from pydantic import BaseModel
from pydantic_settings import BaseSettings


class ResourcesModel(BaseModel):
class ResourcesModel(BaseSettings, env_prefix="resources_"):
public_url: str
6 changes: 4 additions & 2 deletions helpers/config_models/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
from helpers.config_models.openai import OpenAiModel
from helpers.config_models.resources import ResourcesModel
from helpers.config_models.workflow import WorkflowModel
from pydantic import BaseModel
from pydantic import Field
from pydantic_settings import BaseSettings


class RootModel(BaseModel):
class RootModel(BaseSettings, env_prefix=""):
api: ApiModel
cognitive_service: CognitiveServiceModel
communication_service: CommunicationServiceModel
Expand All @@ -19,4 +20,5 @@ class RootModel(BaseModel):
monitoring: MonitoringModel
openai: OpenAiModel
resources: ResourcesModel
version: str = Field(frozen=True)
workflow: WorkflowModel
4 changes: 2 additions & 2 deletions helpers/config_models/workflow.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from pydantic import BaseModel
from pydantic_extra_types.phone_numbers import PhoneNumber
from pydantic_settings import BaseSettings


# E164 is standard accross all Microsoft services
PhoneNumber.phone_format = "E164"


class WorkflowModel(BaseModel):
class WorkflowModel(BaseSettings, env_prefix="workflow_"):
agent_phone_number: PhoneNumber
bot_company: str
bot_name: str
Expand Down
17 changes: 7 additions & 10 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,7 @@


_logger = build_logger(__name__)

ROOT_PATH = CONFIG.api.root_path
AZ_CREDENTIAL = DefaultAzureCredential()
_logger.info(f"claim-ai v{CONFIG.version}")

jinja = Environment(
autoescape=select_autoescape(),
Expand All @@ -59,14 +57,14 @@
api_key=CONFIG.openai.api_key.get_secret_value() if CONFIG.openai.api_key else None,
azure_ad_token_provider=(
get_bearer_token_provider(
AZ_CREDENTIAL, "https://cognitiveservices.azure.com/.default"
DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default"
)
if not CONFIG.openai.api_key
else None
),
)
source_caller = PhoneNumberIdentifier(CONFIG.communication_service.phone_number)
_logger.info(f"Using phone number {source_caller}")
_logger.info(f"Using phone number {str(CONFIG.communication_service.phone_number)}")
# Cannot place calls with RBAC, need to use access key (see: https://learn.microsoft.com/en-us/azure/communication-services/concepts/authentication#authentication-options)
call_automation_client = CallAutomationClient(
endpoint=CONFIG.communication_service.endpoint,
Expand All @@ -75,15 +73,14 @@
),
)
sms_client = SmsClient(
credential=AZ_CREDENTIAL, endpoint=CONFIG.communication_service.endpoint
credential=DefaultAzureCredential(), endpoint=CONFIG.communication_service.endpoint
)
_logger.info(f"Using database {CONFIG.database.mode}")
db = (
SqliteStore(CONFIG.database.sqlite)
if CONFIG.database.mode == DatabaseMode.SQLITE
else CosmosStore(CONFIG.database.cosmos_db)
)
_logger.info(f'Using root path "{ROOT_PATH}"')
_logger.info(f'Using root path "{CONFIG.api.root_path}"')
api = FastAPI(
contact={
"url": "https://github.com/clemlesne/claim-ai-phone-bot",
Expand All @@ -93,9 +90,9 @@
"name": "Apache-2.0",
"url": "https://github.com/clemlesne/claim-ai-phone-bot/blob/master/LICENCE",
},
root_path=ROOT_PATH,
root_path=CONFIG.api.root_path,
title="claim-ai-phone-bot",
version=CONFIG.api.version,
version=CONFIG.version,
)


Expand Down
14 changes: 8 additions & 6 deletions persistence/cosmos.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,19 @@ class CosmosStore(IStore):
_db: ContainerProxy

def __init__(self, config: CosmosDbModel):
_logger.info(f"Using CosmosDB {config.database}/{config.container}")
client = CosmosClient(
connection_timeout=5, credential=AZ_CREDENTIAL, url=config.endpoint
)
database = client.get_database_client(config.database)
self._db = database.get_container_client(config.container)

async def call_aget(self, call_id: UUID) -> Optional[CallModel]:
query = f"SELECT * FROM c WHERE c.call_id = @call_id"
_logger.debug(f"Loading call {call_id}")
try:
items = self._db.query_items(
enable_cross_partition_query=True,
query=query,
query="SELECT * FROM c WHERE c.call_id = @call_id",
parameters=[{"name": "@call_id", "value": call_id}],
)
except CosmosHttpResponseError as e:
Expand All @@ -48,6 +49,7 @@ async def call_aget(self, call_id: UUID) -> Optional[CallModel]:
return None

async def call_aset(self, call: CallModel) -> bool:
_logger.debug(f"Saving call {call.call_id}")
data = jsonable_encoder(call)
_logger.debug(f"Saving call {call.call_id}: {data}")
try:
Expand All @@ -58,12 +60,12 @@ async def call_aset(self, call: CallModel) -> bool:
return False

async def call_asearch_one(self, phone_number: str) -> Optional[CallModel]:
query = f"SELECT * FROM c WHERE c.created_at < @date_limit ORDER BY c.created_at DESC"
_logger.debug(f"Loading last call for {phone_number}")
try:
items = self._db.query_items(
max_item_count=1,
partition_key=phone_number,
query=query,
query="SELECT * FROM c WHERE c.created_at < @date_limit ORDER BY c.created_at DESC",
parameters=[
{
"name": "@date_limit",
Expand All @@ -87,12 +89,12 @@ async def call_asearch_one(self, phone_number: str) -> Optional[CallModel]:
return None

async def call_asearch_all(self, phone_number: str) -> Optional[List[CallModel]]:
_logger.debug(f"Loading all calls for {phone_number}")
calls = []
query = f"SELECT * FROM c ORDER BY c.created_at DESC"
try:
items = self._db.query_items(
partition_key=phone_number,
query=query,
query="SELECT * FROM c ORDER BY c.created_at DESC",
)
except CosmosHttpResponseError as e:
_logger.error(f"Error accessing CosmosDB: {e}")
Expand Down
Loading

0 comments on commit e72e5da

Please sign in to comment.