Spaces:
Runtime error
Runtime error
Commit
·
a3aa6c1
1
Parent(s):
f0c93f5
Refactor Dockerfile and clean up unused schemas; update Redis client initialization and environment configuration
Browse files- .env.example +22 -7
- Dockerfile +0 -4
- src/__init__.py +40 -0
- src/config/__init__.py +1 -8
- src/controllers/__init__.py +0 -8
- src/controllers/_conversation_controller.py +53 -17
- src/controllers/_file_controller.py +1 -13
- src/controllers/_meeting_controller.py +0 -67
- src/middlewares/_authentication.py +7 -9
- src/models/_sessions.py +2 -2
- src/models/_users.py +2 -2
- src/prompts/system_prompt.md +10 -10
- src/repositories/_base_repository.py +1 -1
- src/schemas/__init__.py +1 -2
- src/schemas/_files.py +0 -4
- src/schemas/_sessions.py +1 -0
- src/schemas/_users.py +1 -0
- src/services/_meeting_service.py +10 -3
- src/services/_session_service.py +1 -1
- src/services/_web_rtc_service.py +1 -1
- src/services/_websocket_service.py +8 -2
- src/utils/__init__.py +2 -0
- src/utils/_bcrypt_util.py +4 -4
- src/utils/_jwt_util.py +14 -13
- src/utils/_openai_client.py +7 -92
- src/utils/_openai_tools.py +86 -0
- src/{config/_redis.py → utils/_redis_client.py} +3 -10
.env.example
CHANGED
|
@@ -1,18 +1,33 @@
|
|
| 1 |
# Environment file configuration
|
| 2 |
CORS_ALLOW_ORIGINS="http://localhost, http://127.0.0.1"
|
| 3 |
|
|
|
|
| 4 |
LOG_FILE=app.log
|
| 5 |
LOG_RETENTION="90 days"
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
|
|
|
| 14 |
|
|
|
|
| 15 |
JWT_SECRET_KEY=your-jwt-secret-key
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Environment file configuration
|
| 2 |
CORS_ALLOW_ORIGINS="http://localhost, http://127.0.0.1"
|
| 3 |
|
| 4 |
+
# Logging configuration
|
| 5 |
LOG_FILE=app.log
|
| 6 |
LOG_RETENTION="90 days"
|
| 7 |
|
| 8 |
+
# OPENAI Configuration
|
| 9 |
+
OPENAI_API_KEY=
|
| 10 |
+
OPENAI_BASE_URL=
|
| 11 |
+
OPENAI_WS_BASE_URL=
|
| 12 |
+
OPENAI_REALTIME_MODEL=
|
| 13 |
+
OPENAI_CHAT_COMPLETION_MODEL=
|
| 14 |
|
| 15 |
+
# PINECONE Configuration
|
| 16 |
+
PINECONE_API_KEY=
|
| 17 |
+
PINECONE_INDEX_NAME=
|
| 18 |
|
| 19 |
+
# MONGO Configuration
|
| 20 |
+
MONGO_DB_URI=
|
| 21 |
+
MONGO_DB_NAME=
|
| 22 |
|
| 23 |
+
# JWT Configuration
|
| 24 |
JWT_SECRET_KEY=your-jwt-secret-key
|
| 25 |
|
| 26 |
+
# HUBSPOT Configuration
|
| 27 |
+
HUBSPOT_API_KEY=
|
| 28 |
+
HUBSPOT_BASE_URL=
|
| 29 |
+
|
| 30 |
+
# REDIS Configuration
|
| 31 |
+
REDIS_URI=
|
| 32 |
+
REDIS_SESSION_EXPIRY=
|
| 33 |
+
|
Dockerfile
CHANGED
|
@@ -16,10 +16,6 @@ RUN chown -R user /app
|
|
| 16 |
|
| 17 |
USER user
|
| 18 |
|
| 19 |
-
COPY --chown=user alembic.ini /app/
|
| 20 |
-
|
| 21 |
-
COPY --chown=user migrations /app/migrations
|
| 22 |
-
|
| 23 |
COPY --chown=user src /app/src
|
| 24 |
|
| 25 |
EXPOSE 8000
|
|
|
|
| 16 |
|
| 17 |
USER user
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
COPY --chown=user src /app/src
|
| 20 |
|
| 21 |
EXPOSE 8000
|
src/__init__.py
CHANGED
|
@@ -15,15 +15,55 @@ if not os.getenv("CORS_ALLOW_ORIGINS"):
|
|
| 15 |
"CORS_ALLOW_ORIGINS environment not set. Allowing localhost by default."
|
| 16 |
)
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
if not os.getenv("OPENAI_API_KEY"):
|
| 19 |
raise ValueError("OPENAI_API_KEY environment not set.")
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
if not os.getenv("MONGO_DB_URI"):
|
| 22 |
raise ValueError("MONGO_DB_URI environment not set.")
|
| 23 |
|
| 24 |
if not os.getenv("MONGO_DB_NAME"):
|
| 25 |
raise ValueError("MONGO_DB_NAME environment not set.")
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
__all__ = ["app"]
|
| 28 |
__version__ = "0.1.0"
|
| 29 |
__author__ = "Ramanjit Singh"
|
|
|
|
| 15 |
"CORS_ALLOW_ORIGINS environment not set. Allowing localhost by default."
|
| 16 |
)
|
| 17 |
|
| 18 |
+
if not os.getenv("LOG_FILE"):
|
| 19 |
+
raise ValueError("LOG_FILE environment not set.")
|
| 20 |
+
|
| 21 |
+
if not os.getenv("LOG_RETENTION"):
|
| 22 |
+
logger.warning("LOG_RETENTION environment not set. Defaulting to 90 days.")
|
| 23 |
+
|
| 24 |
if not os.getenv("OPENAI_API_KEY"):
|
| 25 |
raise ValueError("OPENAI_API_KEY environment not set.")
|
| 26 |
|
| 27 |
+
if not os.getenv("OPENAI_BASE_URL"):
|
| 28 |
+
raise ValueError("OPENAI_BASE_URL environment not set.")
|
| 29 |
+
|
| 30 |
+
if not os.getenv("OPENAI_WS_BASE_URL"):
|
| 31 |
+
raise ValueError("OPENAI_WS_BASE_URL environment not set.")
|
| 32 |
+
|
| 33 |
+
if not os.getenv("OPENAI_REALTIME_MODEL"):
|
| 34 |
+
raise ValueError("OPENAI_REALTIME_MODEL environment not set.")
|
| 35 |
+
|
| 36 |
+
if not os.getenv("OPENAI_CHAT_COMPLETION_MODEL"):
|
| 37 |
+
raise ValueError("OPENAI_CHAT_COMPLETION_MODEL environment not set.")
|
| 38 |
+
|
| 39 |
+
if not os.getenv("PINECONE_API_KEY"):
|
| 40 |
+
raise ValueError("PINECONE_API_KEY environment not set.")
|
| 41 |
+
|
| 42 |
+
if not os.getenv("PINECONE_INDEX_NAME"):
|
| 43 |
+
raise ValueError("PINECONE_INDEX_NAME environment not set.")
|
| 44 |
+
|
| 45 |
if not os.getenv("MONGO_DB_URI"):
|
| 46 |
raise ValueError("MONGO_DB_URI environment not set.")
|
| 47 |
|
| 48 |
if not os.getenv("MONGO_DB_NAME"):
|
| 49 |
raise ValueError("MONGO_DB_NAME environment not set.")
|
| 50 |
|
| 51 |
+
if not os.getenv("JWT_SECRET_KEY"):
|
| 52 |
+
raise ValueError("JWT_SECRET_KEY environment not set.")
|
| 53 |
+
|
| 54 |
+
if not os.getenv("HUBSPOT_API_KEY"):
|
| 55 |
+
raise ValueError("HUBSPOT_API_KEY environment not set.")
|
| 56 |
+
|
| 57 |
+
if not os.getenv("HUBSPOT_BASE_URL"):
|
| 58 |
+
raise ValueError("HUBSPOT_BASE_URL environment not set.")
|
| 59 |
+
|
| 60 |
+
if not os.getenv("REDIS_URI"):
|
| 61 |
+
raise ValueError("REDIS_URI environment not set.")
|
| 62 |
+
|
| 63 |
+
if not os.getenv("REDIS_SESSION_EXPIRY"):
|
| 64 |
+
raise ValueError("REDIS_SESSION_EXPIRY environment not set.")
|
| 65 |
+
|
| 66 |
+
|
| 67 |
__all__ = ["app"]
|
| 68 |
__version__ = "0.1.0"
|
| 69 |
__author__ = "Ramanjit Singh"
|
src/config/__init__.py
CHANGED
|
@@ -1,13 +1,6 @@
|
|
| 1 |
from ._logger import logger
|
| 2 |
from ._database import DatabaseConfig
|
| 3 |
-
from ._redis import RedisConfig, redis_client, REDIS_SESSION_EXPIRY
|
| 4 |
|
| 5 |
-
__all__ = [
|
| 6 |
-
"logger",
|
| 7 |
-
"DatabaseConfig",
|
| 8 |
-
"RedisConfig",
|
| 9 |
-
"redis_client",
|
| 10 |
-
"REDIS_SESSION_EXPIRY",
|
| 11 |
-
]
|
| 12 |
__version__ = "0.1.0"
|
| 13 |
__author__ = "Ramanjit Singh"
|
|
|
|
| 1 |
from ._logger import logger
|
| 2 |
from ._database import DatabaseConfig
|
|
|
|
| 3 |
|
| 4 |
+
__all__ = ["logger", "DatabaseConfig"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
__version__ = "0.1.0"
|
| 6 |
__author__ = "Ramanjit Singh"
|
src/controllers/__init__.py
CHANGED
|
@@ -4,14 +4,12 @@ from fastapi.security import APIKeyHeader
|
|
| 4 |
from ._auth_controller import AuthController
|
| 5 |
from ._file_controller import FileController
|
| 6 |
from ._conversation_controller import ConversationController
|
| 7 |
-
from ._meeting_controller import MeetingController
|
| 8 |
|
| 9 |
api_router = APIRouter()
|
| 10 |
|
| 11 |
auth_router = AuthController().api_router
|
| 12 |
conversation_router = ConversationController().api_router
|
| 13 |
file_router = FileController().api_router
|
| 14 |
-
meeting_router = MeetingController().api_router
|
| 15 |
|
| 16 |
API_KEY_HEADER = APIKeyHeader(name="Authorization", auto_error=False)
|
| 17 |
|
|
@@ -33,12 +31,6 @@ api_router.include_router(
|
|
| 33 |
tags=["Semantic Search"],
|
| 34 |
dependencies=[Depends(API_KEY_HEADER)],
|
| 35 |
)
|
| 36 |
-
api_router.include_router(
|
| 37 |
-
meeting_router,
|
| 38 |
-
prefix="/v1",
|
| 39 |
-
tags=["Meeting"],
|
| 40 |
-
dependencies=[Depends(API_KEY_HEADER)],
|
| 41 |
-
)
|
| 42 |
|
| 43 |
__all__ = ["api_router"]
|
| 44 |
__version__ = "0.1.0"
|
|
|
|
| 4 |
from ._auth_controller import AuthController
|
| 5 |
from ._file_controller import FileController
|
| 6 |
from ._conversation_controller import ConversationController
|
|
|
|
| 7 |
|
| 8 |
api_router = APIRouter()
|
| 9 |
|
| 10 |
auth_router = AuthController().api_router
|
| 11 |
conversation_router = ConversationController().api_router
|
| 12 |
file_router = FileController().api_router
|
|
|
|
| 13 |
|
| 14 |
API_KEY_HEADER = APIKeyHeader(name="Authorization", auto_error=False)
|
| 15 |
|
|
|
|
| 31 |
tags=["Semantic Search"],
|
| 32 |
dependencies=[Depends(API_KEY_HEADER)],
|
| 33 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
__all__ = ["api_router"]
|
| 36 |
__version__ = "0.1.0"
|
src/controllers/_conversation_controller.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import
|
| 2 |
from fastapi import (
|
| 3 |
APIRouter,
|
| 4 |
HTTPException,
|
|
@@ -9,7 +9,6 @@ from fastapi import (
|
|
| 9 |
from typing import List
|
| 10 |
|
| 11 |
from src.config import logger
|
| 12 |
-
from src.config import redis_client, REDIS_SESSION_EXPIRY
|
| 13 |
from src.services import ConversationService
|
| 14 |
from src.schemas import (
|
| 15 |
CreateConversationSchema,
|
|
@@ -19,7 +18,7 @@ from src.schemas import (
|
|
| 19 |
CreateConversationSummaryResponse,
|
| 20 |
CreateWebrtcConnectionResponse,
|
| 21 |
)
|
| 22 |
-
from src.utils import JWTUtil
|
| 23 |
|
| 24 |
|
| 25 |
class ConnectionManager:
|
|
@@ -27,9 +26,10 @@ class ConnectionManager:
|
|
| 27 |
self.jwt = JWTUtil()
|
| 28 |
self.active_connections: List[WebSocket] = []
|
| 29 |
|
| 30 |
-
async def connect(self, websocket: WebSocket):
|
| 31 |
await websocket.accept()
|
| 32 |
token = websocket.query_params.get("token")
|
|
|
|
| 33 |
user = self.jwt.validate_jwt(token)
|
| 34 |
|
| 35 |
if not user:
|
|
@@ -37,30 +37,41 @@ class ConnectionManager:
|
|
| 37 |
return None
|
| 38 |
|
| 39 |
self.active_connections.append(websocket)
|
| 40 |
-
await self._update_redis_status(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
return user
|
| 42 |
|
| 43 |
-
async def disconnect(
|
|
|
|
|
|
|
| 44 |
if websocket in self.active_connections:
|
| 45 |
self.active_connections.remove(websocket)
|
| 46 |
|
| 47 |
-
await self._update_redis_status(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
-
async def _update_redis_status(self,
|
| 50 |
if status:
|
| 51 |
await redis_client.set(
|
| 52 |
-
f"session:{
|
| 53 |
)
|
| 54 |
else:
|
| 55 |
-
await redis_client.delete(f"session:{
|
| 56 |
|
| 57 |
-
redis_status = await redis_client.get(f"session:{
|
| 58 |
-
logger.info(f"Redis status for user {
|
| 59 |
|
| 60 |
|
| 61 |
class ConversationController:
|
| 62 |
def __init__(self):
|
| 63 |
self.websocket_connection_manager = ConnectionManager()
|
|
|
|
| 64 |
self.service = ConversationService
|
| 65 |
self.api_router = APIRouter()
|
| 66 |
self.api_router.add_api_route(
|
|
@@ -70,7 +81,7 @@ class ConversationController:
|
|
| 70 |
response_model=CreateConversationResponse,
|
| 71 |
)
|
| 72 |
self.api_router.add_api_route(
|
| 73 |
-
"/conversations/{conversation_id}
|
| 74 |
self.create_webrtc_connection,
|
| 75 |
methods=["POST"],
|
| 76 |
response_model=CreateWebrtcConnectionResponse,
|
|
@@ -92,6 +103,9 @@ class ConversationController:
|
|
| 92 |
return await service.create_conversation(
|
| 93 |
user_id=user_id, modality=data.modality
|
| 94 |
)
|
|
|
|
|
|
|
|
|
|
| 95 |
except Exception as e:
|
| 96 |
logger.error(f"Error creating conversation: {str(e)}")
|
| 97 |
raise HTTPException(status_code=500, detail="Failed to create conversation")
|
|
@@ -109,6 +123,9 @@ class ConversationController:
|
|
| 109 |
conversation_id=data.conversation_id,
|
| 110 |
offer=data.offer.model_dump(),
|
| 111 |
)
|
|
|
|
|
|
|
|
|
|
| 112 |
except Exception as e:
|
| 113 |
logger.error(f"Error creating WebRTC connection: {str(e)}")
|
| 114 |
raise HTTPException(
|
|
@@ -116,7 +133,9 @@ class ConversationController:
|
|
| 116 |
)
|
| 117 |
|
| 118 |
async def conversation(self, websocket: WebSocket):
|
| 119 |
-
user = await self.websocket_connection_manager.connect(
|
|
|
|
|
|
|
| 120 |
if not user:
|
| 121 |
return
|
| 122 |
user_id = user["user_id"]
|
|
@@ -129,15 +148,28 @@ class ConversationController:
|
|
| 129 |
user_id=user_id,
|
| 130 |
conversation_id=conversation_id,
|
| 131 |
modality=modality,
|
| 132 |
-
redis_client=redis_client,
|
| 133 |
)
|
| 134 |
except WebSocketDisconnect:
|
| 135 |
-
await self.websocket_connection_manager.disconnect(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
logger.info("WebSocket connection closed")
|
| 137 |
|
|
|
|
|
|
|
|
|
|
| 138 |
except Exception as e:
|
| 139 |
logger.error(f"Error in WebSocket conversation: {str(e)}")
|
| 140 |
-
await self.websocket_connection_manager.disconnect(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
async def create_conversation_summary(
|
| 143 |
self, request: Request, data: CreateConversationSummarySchema
|
|
@@ -148,6 +180,10 @@ class ConversationController:
|
|
| 148 |
return await service.create_conversation_summary(
|
| 149 |
user_id=user_id, conversation_id=data.conversation_id
|
| 150 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
except Exception as e:
|
| 152 |
logger.error(f"Error creating conversation summary: {str(e)}")
|
| 153 |
raise HTTPException(
|
|
|
|
| 1 |
+
import os
|
| 2 |
from fastapi import (
|
| 3 |
APIRouter,
|
| 4 |
HTTPException,
|
|
|
|
| 9 |
from typing import List
|
| 10 |
|
| 11 |
from src.config import logger
|
|
|
|
| 12 |
from src.services import ConversationService
|
| 13 |
from src.schemas import (
|
| 14 |
CreateConversationSchema,
|
|
|
|
| 18 |
CreateConversationSummaryResponse,
|
| 19 |
CreateWebrtcConnectionResponse,
|
| 20 |
)
|
| 21 |
+
from src.utils import JWTUtil, RedisClient
|
| 22 |
|
| 23 |
|
| 24 |
class ConnectionManager:
|
|
|
|
| 26 |
self.jwt = JWTUtil()
|
| 27 |
self.active_connections: List[WebSocket] = []
|
| 28 |
|
| 29 |
+
async def connect(self, websocket: WebSocket, redis_client):
|
| 30 |
await websocket.accept()
|
| 31 |
token = websocket.query_params.get("token")
|
| 32 |
+
conversation_id = websocket.query_params.get("conversation_id")
|
| 33 |
user = self.jwt.validate_jwt(token)
|
| 34 |
|
| 35 |
if not user:
|
|
|
|
| 37 |
return None
|
| 38 |
|
| 39 |
self.active_connections.append(websocket)
|
| 40 |
+
await self._update_redis_status(
|
| 41 |
+
unique_id=f"{user["user_id"]}{conversation_id}",
|
| 42 |
+
redis_client=redis_client,
|
| 43 |
+
status="active",
|
| 44 |
+
)
|
| 45 |
return user
|
| 46 |
|
| 47 |
+
async def disconnect(
|
| 48 |
+
self, websocket: WebSocket, redis_client, user_id: str, conversation_id: str
|
| 49 |
+
):
|
| 50 |
if websocket in self.active_connections:
|
| 51 |
self.active_connections.remove(websocket)
|
| 52 |
|
| 53 |
+
await self._update_redis_status(
|
| 54 |
+
unique_id=f"{user_id}{conversation_id}",
|
| 55 |
+
redis_client=redis_client,
|
| 56 |
+
status=None,
|
| 57 |
+
)
|
| 58 |
|
| 59 |
+
async def _update_redis_status(self, unique_id: str, redis_client, status: str):
|
| 60 |
if status:
|
| 61 |
await redis_client.set(
|
| 62 |
+
f"session:{unique_id}", status, ex=os.getenv("REDIS_SESSION_EXPIRY")
|
| 63 |
)
|
| 64 |
else:
|
| 65 |
+
await redis_client.delete(f"session:{unique_id}")
|
| 66 |
|
| 67 |
+
redis_status = await redis_client.get(f"session:{unique_id}")
|
| 68 |
+
logger.info(f"Redis status for user {unique_id}: {redis_status}")
|
| 69 |
|
| 70 |
|
| 71 |
class ConversationController:
|
| 72 |
def __init__(self):
|
| 73 |
self.websocket_connection_manager = ConnectionManager()
|
| 74 |
+
self.redis_client = RedisClient().client
|
| 75 |
self.service = ConversationService
|
| 76 |
self.api_router = APIRouter()
|
| 77 |
self.api_router.add_api_route(
|
|
|
|
| 81 |
response_model=CreateConversationResponse,
|
| 82 |
)
|
| 83 |
self.api_router.add_api_route(
|
| 84 |
+
"/conversations/{conversation_id}",
|
| 85 |
self.create_webrtc_connection,
|
| 86 |
methods=["POST"],
|
| 87 |
response_model=CreateWebrtcConnectionResponse,
|
|
|
|
| 103 |
return await service.create_conversation(
|
| 104 |
user_id=user_id, modality=data.modality
|
| 105 |
)
|
| 106 |
+
except HTTPException as e:
|
| 107 |
+
raise
|
| 108 |
+
|
| 109 |
except Exception as e:
|
| 110 |
logger.error(f"Error creating conversation: {str(e)}")
|
| 111 |
raise HTTPException(status_code=500, detail="Failed to create conversation")
|
|
|
|
| 123 |
conversation_id=data.conversation_id,
|
| 124 |
offer=data.offer.model_dump(),
|
| 125 |
)
|
| 126 |
+
except HTTPException as e:
|
| 127 |
+
raise
|
| 128 |
+
|
| 129 |
except Exception as e:
|
| 130 |
logger.error(f"Error creating WebRTC connection: {str(e)}")
|
| 131 |
raise HTTPException(
|
|
|
|
| 133 |
)
|
| 134 |
|
| 135 |
async def conversation(self, websocket: WebSocket):
|
| 136 |
+
user = await self.websocket_connection_manager.connect(
|
| 137 |
+
websocket=websocket, redis_client=self.redis_client
|
| 138 |
+
)
|
| 139 |
if not user:
|
| 140 |
return
|
| 141 |
user_id = user["user_id"]
|
|
|
|
| 148 |
user_id=user_id,
|
| 149 |
conversation_id=conversation_id,
|
| 150 |
modality=modality,
|
| 151 |
+
redis_client=self.redis_client,
|
| 152 |
)
|
| 153 |
except WebSocketDisconnect:
|
| 154 |
+
await self.websocket_connection_manager.disconnect(
|
| 155 |
+
websocket=websocket,
|
| 156 |
+
user_id=user_id,
|
| 157 |
+
redis_client=self.redis_client,
|
| 158 |
+
conversation_id=conversation_id,
|
| 159 |
+
)
|
| 160 |
logger.info("WebSocket connection closed")
|
| 161 |
|
| 162 |
+
except HTTPException as e:
|
| 163 |
+
raise
|
| 164 |
+
|
| 165 |
except Exception as e:
|
| 166 |
logger.error(f"Error in WebSocket conversation: {str(e)}")
|
| 167 |
+
await self.websocket_connection_manager.disconnect(
|
| 168 |
+
websocket=websocket,
|
| 169 |
+
user_id=user_id,
|
| 170 |
+
redis_client=self.redis_client,
|
| 171 |
+
conversation_id=conversation_id,
|
| 172 |
+
)
|
| 173 |
|
| 174 |
async def create_conversation_summary(
|
| 175 |
self, request: Request, data: CreateConversationSummarySchema
|
|
|
|
| 180 |
return await service.create_conversation_summary(
|
| 181 |
user_id=user_id, conversation_id=data.conversation_id
|
| 182 |
)
|
| 183 |
+
|
| 184 |
+
except HTTPException as e:
|
| 185 |
+
raise
|
| 186 |
+
|
| 187 |
except Exception as e:
|
| 188 |
logger.error(f"Error creating conversation summary: {str(e)}")
|
| 189 |
raise HTTPException(
|
src/controllers/_file_controller.py
CHANGED
|
@@ -2,7 +2,7 @@ from fastapi import APIRouter, HTTPException, Request
|
|
| 2 |
|
| 3 |
from src.config import logger
|
| 4 |
from src.services import FileService
|
| 5 |
-
from src.schemas import InsertFileSchema
|
| 6 |
|
| 7 |
|
| 8 |
class FileController:
|
|
@@ -10,9 +10,6 @@ class FileController:
|
|
| 10 |
self.service = FileService
|
| 11 |
self.api_router = APIRouter()
|
| 12 |
self.api_router.add_api_route("/files", self.insert_file, methods=["POST"])
|
| 13 |
-
self.api_router.add_api_route(
|
| 14 |
-
"/files/search", self.semantic_search, methods=["GET"]
|
| 15 |
-
)
|
| 16 |
|
| 17 |
async def insert_file(self, request: Request, data: InsertFileSchema):
|
| 18 |
try:
|
|
@@ -24,12 +21,3 @@ class FileController:
|
|
| 24 |
except Exception as e:
|
| 25 |
logger.error(f"Error while inserting file: {str(e)}")
|
| 26 |
raise HTTPException(status_code=500, detail=str(e))
|
| 27 |
-
|
| 28 |
-
async def semantic_search(self, request: Request, data: SemanticSearchSchema):
|
| 29 |
-
try:
|
| 30 |
-
query = data.query
|
| 31 |
-
async with self.service() as service:
|
| 32 |
-
return await service.semantic_search(query=query)
|
| 33 |
-
except Exception as e:
|
| 34 |
-
logger.error(f"Error while searching for similar data: {str(e)}")
|
| 35 |
-
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
| 2 |
|
| 3 |
from src.config import logger
|
| 4 |
from src.services import FileService
|
| 5 |
+
from src.schemas import InsertFileSchema
|
| 6 |
|
| 7 |
|
| 8 |
class FileController:
|
|
|
|
| 10 |
self.service = FileService
|
| 11 |
self.api_router = APIRouter()
|
| 12 |
self.api_router.add_api_route("/files", self.insert_file, methods=["POST"])
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
async def insert_file(self, request: Request, data: InsertFileSchema):
|
| 15 |
try:
|
|
|
|
| 21 |
except Exception as e:
|
| 22 |
logger.error(f"Error while inserting file: {str(e)}")
|
| 23 |
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/controllers/_meeting_controller.py
DELETED
|
@@ -1,67 +0,0 @@
|
|
| 1 |
-
from fastapi import APIRouter, HTTPException, Request
|
| 2 |
-
|
| 3 |
-
from src.config import logger
|
| 4 |
-
from src.schemas import DemoBookingSchema
|
| 5 |
-
from src.services import MeetingService
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
class MeetingController:
|
| 9 |
-
def __init__(self):
|
| 10 |
-
self.service = MeetingService
|
| 11 |
-
self.api_router = APIRouter()
|
| 12 |
-
self.api_router.add_api_route(
|
| 13 |
-
"/meetings/slots",
|
| 14 |
-
self.get_available_slots,
|
| 15 |
-
methods=["GET"],
|
| 16 |
-
)
|
| 17 |
-
self.api_router.add_api_route(
|
| 18 |
-
"/meetings/book",
|
| 19 |
-
self.schedule_meeting,
|
| 20 |
-
methods=["POST"],
|
| 21 |
-
)
|
| 22 |
-
self.api_router.add_api_route(
|
| 23 |
-
"/meetings/confirm",
|
| 24 |
-
self.send_meeting_confirmation,
|
| 25 |
-
methods=["POST"],
|
| 26 |
-
)
|
| 27 |
-
|
| 28 |
-
async def get_available_slots(
|
| 29 |
-
self, request: Request, salesperson_meeting_id: str, timezone: str
|
| 30 |
-
):
|
| 31 |
-
try:
|
| 32 |
-
async with self.service() as service:
|
| 33 |
-
return await service.get_available_slots(
|
| 34 |
-
salesperson_meeting_id, timezone
|
| 35 |
-
)
|
| 36 |
-
except Exception as e:
|
| 37 |
-
logger.error(f"Error getting available slots: {str(e)}")
|
| 38 |
-
raise HTTPException(status_code=500, detail="Failed to get available slots")
|
| 39 |
-
|
| 40 |
-
async def schedule_meeting(self, request: Request, data: DemoBookingSchema):
|
| 41 |
-
try:
|
| 42 |
-
user_id = request.state.user["user_id"]
|
| 43 |
-
async with self.service() as service:
|
| 44 |
-
return await service.schedule_meeting(
|
| 45 |
-
user_id=user_id,
|
| 46 |
-
salesperson_meeting_id=data.salesperson_meeting_id,
|
| 47 |
-
duration=data.duration,
|
| 48 |
-
preferred_start_time=data.preferred_start_time,
|
| 49 |
-
timezone=data.timezone,
|
| 50 |
-
locale=data.locale,
|
| 51 |
-
)
|
| 52 |
-
except Exception as e:
|
| 53 |
-
logger.error(f"Error booking demo: {str(e)}")
|
| 54 |
-
raise HTTPException(
|
| 55 |
-
status_code=500, detail=f"Failed to book demo: {str(e)}"
|
| 56 |
-
)
|
| 57 |
-
|
| 58 |
-
async def send_meeting_confirmation(self, request: Request):
|
| 59 |
-
try:
|
| 60 |
-
user_id = request.state.user["user_id"]
|
| 61 |
-
async with self.service() as service:
|
| 62 |
-
return await service.send_meeting_confirmation(user_id=user_id)
|
| 63 |
-
except Exception as e:
|
| 64 |
-
logger.error(f"Error sending meeting confirmation: {str(e)}")
|
| 65 |
-
raise HTTPException(
|
| 66 |
-
status_code=500, detail="Failed to send meeting confirmation"
|
| 67 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/middlewares/_authentication.py
CHANGED
|
@@ -39,31 +39,29 @@ class AuthenticationMiddleware(BaseHTTPMiddleware):
|
|
| 39 |
except HTTPException as e:
|
| 40 |
# Create a response with CORS headers
|
| 41 |
response = JSONResponse(
|
| 42 |
-
status_code=e.status_code,
|
| 43 |
-
content={"detail": e.detail}
|
| 44 |
)
|
| 45 |
-
|
| 46 |
origin = request.headers.get("origin", "*")
|
| 47 |
response.headers["Access-Control-Allow-Origin"] = origin
|
| 48 |
response.headers["Access-Control-Allow-Credentials"] = "true"
|
| 49 |
response.headers["Access-Control-Allow-Methods"] = "*"
|
| 50 |
response.headers["Access-Control-Allow-Headers"] = "*"
|
| 51 |
-
|
| 52 |
return response
|
| 53 |
except Exception as e:
|
| 54 |
response = JSONResponse(
|
| 55 |
-
status_code=500,
|
| 56 |
-
content={"detail": "Internal server error"}
|
| 57 |
)
|
| 58 |
-
|
| 59 |
origin = request.headers.get("origin", "*")
|
| 60 |
response.headers["Access-Control-Allow-Origin"] = origin
|
| 61 |
response.headers["Access-Control-Allow-Credentials"] = "true"
|
| 62 |
response.headers["Access-Control-Allow-Methods"] = "*"
|
| 63 |
response.headers["Access-Control-Allow-Headers"] = "*"
|
| 64 |
-
|
| 65 |
return response
|
| 66 |
-
|
| 67 |
return await call_next(request)
|
| 68 |
|
| 69 |
def _require_auth(self, path: str):
|
|
|
|
| 39 |
except HTTPException as e:
|
| 40 |
# Create a response with CORS headers
|
| 41 |
response = JSONResponse(
|
| 42 |
+
status_code=e.status_code, content={"detail": e.detail}
|
|
|
|
| 43 |
)
|
| 44 |
+
|
| 45 |
origin = request.headers.get("origin", "*")
|
| 46 |
response.headers["Access-Control-Allow-Origin"] = origin
|
| 47 |
response.headers["Access-Control-Allow-Credentials"] = "true"
|
| 48 |
response.headers["Access-Control-Allow-Methods"] = "*"
|
| 49 |
response.headers["Access-Control-Allow-Headers"] = "*"
|
| 50 |
+
|
| 51 |
return response
|
| 52 |
except Exception as e:
|
| 53 |
response = JSONResponse(
|
| 54 |
+
status_code=500, content={"detail": "Internal server error"}
|
|
|
|
| 55 |
)
|
| 56 |
+
|
| 57 |
origin = request.headers.get("origin", "*")
|
| 58 |
response.headers["Access-Control-Allow-Origin"] = origin
|
| 59 |
response.headers["Access-Control-Allow-Credentials"] = "true"
|
| 60 |
response.headers["Access-Control-Allow-Methods"] = "*"
|
| 61 |
response.headers["Access-Control-Allow-Headers"] = "*"
|
| 62 |
+
|
| 63 |
return response
|
| 64 |
+
|
| 65 |
return await call_next(request)
|
| 66 |
|
| 67 |
def _require_auth(self, path: str):
|
src/models/_sessions.py
CHANGED
|
@@ -8,8 +8,8 @@ class Session(Document):
|
|
| 8 |
user_id: Link[User]
|
| 9 |
token: str
|
| 10 |
device_ipv4_address: str
|
| 11 |
-
created_at: datetime = Field(default_factory=datetime.now)
|
| 12 |
-
expired_at: datetime = Field(default_factory=datetime.now)
|
| 13 |
|
| 14 |
class Settings:
|
| 15 |
name = "sessions"
|
|
|
|
| 8 |
user_id: Link[User]
|
| 9 |
token: str
|
| 10 |
device_ipv4_address: str
|
| 11 |
+
created_at: datetime = Field(default_factory=datetime.now)
|
| 12 |
+
expired_at: datetime = Field(default_factory=datetime.now)
|
| 13 |
|
| 14 |
class Settings:
|
| 15 |
name = "sessions"
|
src/models/_users.py
CHANGED
|
@@ -9,9 +9,9 @@ class User(Document):
|
|
| 9 |
first_name: str
|
| 10 |
last_name: str
|
| 11 |
hashed_password: str
|
| 12 |
-
email: Annotated[
|
| 13 |
created_at: datetime = Field(default_factory=datetime.now)
|
| 14 |
updated_at: datetime = Field(default_factory=datetime.now)
|
| 15 |
|
| 16 |
class Settings:
|
| 17 |
-
name = "users"
|
|
|
|
| 9 |
first_name: str
|
| 10 |
last_name: str
|
| 11 |
hashed_password: str
|
| 12 |
+
email: Annotated[str, Indexed(unique=True)]
|
| 13 |
created_at: datetime = Field(default_factory=datetime.now)
|
| 14 |
updated_at: datetime = Field(default_factory=datetime.now)
|
| 15 |
|
| 16 |
class Settings:
|
| 17 |
+
name = "users"
|
src/prompts/system_prompt.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
#
|
| 2 |
|
| 3 |
You are Olivia, a dynamic and supportive virtual assistant, here to deliver personalized, context-aware interactions. Below are key attributes to guide your behavior and tone:
|
| 4 |
|
|
@@ -52,7 +52,7 @@ Always use **get_relevant_information** if the user query relates to **Keepme Sa
|
|
| 52 |
|
| 53 |
- **Description:** Get relevant information (Context) to better respond to user queries.
|
| 54 |
- **Parameters:**
|
| 55 |
-
|
| 56 |
|
| 57 |
#### **get_available_salesperson_meetings**
|
| 58 |
|
|
@@ -62,16 +62,16 @@ Always use **get_relevant_information** if the user query relates to **Keepme Sa
|
|
| 62 |
|
| 63 |
- **Description:** Get available slots for a salesperson meeting.
|
| 64 |
- **Parameters:**
|
| 65 |
-
|
| 66 |
-
|
| 67 |
|
| 68 |
#### **schedule_meeting**
|
| 69 |
|
| 70 |
- **Description:** Schedule a meeting.
|
| 71 |
- **Parameters:**
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
| 1 |
+
# Olivia - Your Friendly and Knowledgeable Assistant
|
| 2 |
|
| 3 |
You are Olivia, a dynamic and supportive virtual assistant, here to deliver personalized, context-aware interactions. Below are key attributes to guide your behavior and tone:
|
| 4 |
|
|
|
|
| 52 |
|
| 53 |
- **Description:** Get relevant information (Context) to better respond to user queries.
|
| 54 |
- **Parameters:**
|
| 55 |
+
- `query` (string): The user query to get relevant information for.
|
| 56 |
|
| 57 |
#### **get_available_salesperson_meetings**
|
| 58 |
|
|
|
|
| 62 |
|
| 63 |
- **Description:** Get available slots for a salesperson meeting.
|
| 64 |
- **Parameters:**
|
| 65 |
+
- `salesperson_meeting_id` (string): The ID of the salesperson meeting.
|
| 66 |
+
- `timezone` (string): The timezone for the available slots.
|
| 67 |
|
| 68 |
#### **schedule_meeting**
|
| 69 |
|
| 70 |
- **Description:** Schedule a meeting.
|
| 71 |
- **Parameters:**
|
| 72 |
+
- `user_id` (string): The ID of the user.
|
| 73 |
+
- `salesperson_meeting_id` (string): The ID of the salesperson meeting.
|
| 74 |
+
- `preferred_start_time` (string, format: date-time): The preferred start time for the meeting.
|
| 75 |
+
- `duration` (integer): The duration of the meeting in minutes.
|
| 76 |
+
- `timezone` (string): The timezone for the meeting.
|
| 77 |
+
- `locale` (string, default: "en-us"): The locale for the meeting.
|
src/repositories/_base_repository.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
from beanie import Document, WriteRules, DeleteRules
|
| 2 |
-
from typing import Type, TypeVar,
|
| 3 |
|
| 4 |
T = TypeVar("T", bound=Document)
|
| 5 |
|
|
|
|
| 1 |
from beanie import Document, WriteRules, DeleteRules
|
| 2 |
+
from typing import Type, TypeVar, Dict
|
| 3 |
|
| 4 |
T = TypeVar("T", bound=Document)
|
| 5 |
|
src/schemas/__init__.py
CHANGED
|
@@ -15,7 +15,7 @@ from ._conversations import (
|
|
| 15 |
CreateWebrtcConnectionResponse,
|
| 16 |
)
|
| 17 |
from ._meetings import DemoBookingSchema
|
| 18 |
-
from ._files import InsertFileSchema
|
| 19 |
|
| 20 |
|
| 21 |
__all__ = [
|
|
@@ -32,7 +32,6 @@ __all__ = [
|
|
| 32 |
"CreateConversationSummaryResponse",
|
| 33 |
"CreateWebrtcConnectionResponse",
|
| 34 |
"InsertFileSchema",
|
| 35 |
-
"SemanticSearchSchema",
|
| 36 |
"DemoBookingSchema",
|
| 37 |
]
|
| 38 |
|
|
|
|
| 15 |
CreateWebrtcConnectionResponse,
|
| 16 |
)
|
| 17 |
from ._meetings import DemoBookingSchema
|
| 18 |
+
from ._files import InsertFileSchema
|
| 19 |
|
| 20 |
|
| 21 |
__all__ = [
|
|
|
|
| 32 |
"CreateConversationSummaryResponse",
|
| 33 |
"CreateWebrtcConnectionResponse",
|
| 34 |
"InsertFileSchema",
|
|
|
|
| 35 |
"DemoBookingSchema",
|
| 36 |
]
|
| 37 |
|
src/schemas/_files.py
CHANGED
|
@@ -4,7 +4,3 @@ from fastapi import UploadFile, File
|
|
| 4 |
|
| 5 |
class InsertFileSchema(BaseModel):
|
| 6 |
file: UploadFile = File(...)
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
class SemanticSearchSchema(BaseModel):
|
| 10 |
-
query: str
|
|
|
|
| 4 |
|
| 5 |
class InsertFileSchema(BaseModel):
|
| 6 |
file: UploadFile = File(...)
|
|
|
|
|
|
|
|
|
|
|
|
src/schemas/_sessions.py
CHANGED
|
@@ -3,6 +3,7 @@ from datetime import datetime
|
|
| 3 |
from pydantic import BaseModel
|
| 4 |
from src.models import User
|
| 5 |
|
|
|
|
| 6 |
class SessionCreateSchema(BaseModel):
|
| 7 |
user_id: User
|
| 8 |
token: str
|
|
|
|
| 3 |
from pydantic import BaseModel
|
| 4 |
from src.models import User
|
| 5 |
|
| 6 |
+
|
| 7 |
class SessionCreateSchema(BaseModel):
|
| 8 |
user_id: User
|
| 9 |
token: str
|
src/schemas/_users.py
CHANGED
|
@@ -35,5 +35,6 @@ class UserSignInSchema(BaseModel):
|
|
| 35 |
class UserSignInResponse(BaseModel):
|
| 36 |
token: str
|
| 37 |
|
|
|
|
| 38 |
class UserSignUpResponse(BaseModel):
|
| 39 |
user_id: str
|
|
|
|
| 35 |
class UserSignInResponse(BaseModel):
|
| 36 |
token: str
|
| 37 |
|
| 38 |
+
|
| 39 |
class UserSignUpResponse(BaseModel):
|
| 40 |
user_id: str
|
src/services/_meeting_service.py
CHANGED
|
@@ -20,7 +20,9 @@ class MeetingService:
|
|
| 20 |
pass
|
| 21 |
|
| 22 |
async def get_available_salesperson_meetings(self):
|
| 23 |
-
request_url =
|
|
|
|
|
|
|
| 24 |
headers = {"authorization": f"Bearer {os.getenv('HUBSPOT_API_KEY')}"}
|
| 25 |
|
| 26 |
async with aiohttp.ClientSession() as session:
|
|
@@ -44,7 +46,10 @@ class MeetingService:
|
|
| 44 |
return result
|
| 45 |
|
| 46 |
async def get_available_slots(self, salesperson_meeting_id: str, timezone: str):
|
| 47 |
-
request_url =
|
|
|
|
|
|
|
|
|
|
| 48 |
headers = {"authorization": f"Bearer {os.getenv('HUBSPOT_API_KEY')}"}
|
| 49 |
|
| 50 |
async with aiohttp.ClientSession() as session:
|
|
@@ -98,7 +103,9 @@ class MeetingService:
|
|
| 98 |
|
| 99 |
duration_ms = duration * 60000
|
| 100 |
|
| 101 |
-
url =
|
|
|
|
|
|
|
| 102 |
headers = {
|
| 103 |
"authorization": f"Bearer {os.getenv('HUBSPOT_API_KEY')}",
|
| 104 |
"content-type": "application/json",
|
|
|
|
| 20 |
pass
|
| 21 |
|
| 22 |
async def get_available_salesperson_meetings(self):
|
| 23 |
+
request_url = (
|
| 24 |
+
os.getenv("HUBSPOT_BASE_URL") + "/scheduler/v3/meetings/meeting-links"
|
| 25 |
+
)
|
| 26 |
headers = {"authorization": f"Bearer {os.getenv('HUBSPOT_API_KEY')}"}
|
| 27 |
|
| 28 |
async with aiohttp.ClientSession() as session:
|
|
|
|
| 46 |
return result
|
| 47 |
|
| 48 |
async def get_available_slots(self, salesperson_meeting_id: str, timezone: str):
|
| 49 |
+
request_url = (
|
| 50 |
+
os.getenv("HUBSPOT_BASE_URL")
|
| 51 |
+
+ "/scheduler/v3/meetings/meeting-links/book/availability-page/{salesperson_meeting_id}?timezone={timezone}"
|
| 52 |
+
)
|
| 53 |
headers = {"authorization": f"Bearer {os.getenv('HUBSPOT_API_KEY')}"}
|
| 54 |
|
| 55 |
async with aiohttp.ClientSession() as session:
|
|
|
|
| 103 |
|
| 104 |
duration_ms = duration * 60000
|
| 105 |
|
| 106 |
+
url = (
|
| 107 |
+
os.getenv("HUBSPOT_BASE_URL") + "/scheduler/v3/meetings/meeting-links/book"
|
| 108 |
+
)
|
| 109 |
headers = {
|
| 110 |
"authorization": f"Bearer {os.getenv('HUBSPOT_API_KEY')}",
|
| 111 |
"content-type": "application/json",
|
src/services/_session_service.py
CHANGED
|
@@ -25,7 +25,7 @@ class SessionService:
|
|
| 25 |
|
| 26 |
async def delete_session(self, session_id):
|
| 27 |
return await self.session_repository.delete(session_id)
|
| 28 |
-
|
| 29 |
async def get_session_by_token(self, token: str):
|
| 30 |
session = await self.session_repository.get_all(filter_by={"token": token})
|
| 31 |
return session[0] if session else None
|
|
|
|
| 25 |
|
| 26 |
async def delete_session(self, session_id):
|
| 27 |
return await self.session_repository.delete(session_id)
|
| 28 |
+
|
| 29 |
async def get_session_by_token(self, token: str):
|
| 30 |
session = await self.session_repository.get_all(filter_by={"token": token})
|
| 31 |
return session[0] if session else None
|
src/services/_web_rtc_service.py
CHANGED
|
@@ -24,7 +24,7 @@ class WebRTCService:
|
|
| 24 |
data = await websocket.receive_text()
|
| 25 |
message = json.loads(data)
|
| 26 |
|
| 27 |
-
user_status = await redis_client.get(f"session:{user_id}")
|
| 28 |
if not user_status:
|
| 29 |
raise Exception("A max 25 minute session has ended")
|
| 30 |
|
|
|
|
| 24 |
data = await websocket.receive_text()
|
| 25 |
message = json.loads(data)
|
| 26 |
|
| 27 |
+
user_status = await redis_client.get(f"session:{user_id}{conversation_id}")
|
| 28 |
if not user_status:
|
| 29 |
raise Exception("A max 25 minute session has ended")
|
| 30 |
|
src/services/_websocket_service.py
CHANGED
|
@@ -30,7 +30,11 @@ class WebSocketService:
|
|
| 30 |
):
|
| 31 |
openai_websocket = None
|
| 32 |
async with websockets.connect(
|
| 33 |
-
uri=
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
additional_headers={
|
| 35 |
"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}",
|
| 36 |
"OpenAI-Beta": "realtime=v1",
|
|
@@ -122,7 +126,9 @@ class WebSocketService:
|
|
| 122 |
user_message = await client_websocket.receive_text()
|
| 123 |
user_message = json.loads(user_message)["user_message"]
|
| 124 |
|
| 125 |
-
user_status = await redis_client.get(
|
|
|
|
|
|
|
| 126 |
if not user_status:
|
| 127 |
raise Exception("A max 25 minute session has ended")
|
| 128 |
|
|
|
|
| 30 |
):
|
| 31 |
openai_websocket = None
|
| 32 |
async with websockets.connect(
|
| 33 |
+
uri=(
|
| 34 |
+
os.getenv("OPENAI_WS_BASE_URL")
|
| 35 |
+
+ "/v1/realtime?model="
|
| 36 |
+
+ os.getenv("OPENAI_REALTIME_MODEL")
|
| 37 |
+
),
|
| 38 |
additional_headers={
|
| 39 |
"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}",
|
| 40 |
"OpenAI-Beta": "realtime=v1",
|
|
|
|
| 126 |
user_message = await client_websocket.receive_text()
|
| 127 |
user_message = json.loads(user_message)["user_message"]
|
| 128 |
|
| 129 |
+
user_status = await redis_client.get(
|
| 130 |
+
f"session:{user_id}{conversation_id}"
|
| 131 |
+
)
|
| 132 |
if not user_status:
|
| 133 |
raise Exception("A max 25 minute session has ended")
|
| 134 |
|
src/utils/__init__.py
CHANGED
|
@@ -3,6 +3,7 @@ from ._bcrypt_util import BcryptUtil
|
|
| 3 |
from ._openai_client import OpenAIClient
|
| 4 |
from ._pinecone_client import PineconeClient
|
| 5 |
from ._meeting_utils import MeetingUtils
|
|
|
|
| 6 |
|
| 7 |
__all__ = [
|
| 8 |
"JWTUtil",
|
|
@@ -10,6 +11,7 @@ __all__ = [
|
|
| 10 |
"OpenAIClient",
|
| 11 |
"PineconeClient",
|
| 12 |
"MeetingUtils",
|
|
|
|
| 13 |
]
|
| 14 |
|
| 15 |
__version__ = "0.1.0"
|
|
|
|
| 3 |
from ._openai_client import OpenAIClient
|
| 4 |
from ._pinecone_client import PineconeClient
|
| 5 |
from ._meeting_utils import MeetingUtils
|
| 6 |
+
from ._redis_client import RedisClient
|
| 7 |
|
| 8 |
__all__ = [
|
| 9 |
"JWTUtil",
|
|
|
|
| 11 |
"OpenAIClient",
|
| 12 |
"PineconeClient",
|
| 13 |
"MeetingUtils",
|
| 14 |
+
"RedisClient",
|
| 15 |
]
|
| 16 |
|
| 17 |
__version__ = "0.1.0"
|
src/utils/_bcrypt_util.py
CHANGED
|
@@ -6,12 +6,12 @@ class BcryptUtil:
|
|
| 6 |
def hash_password(password: str) -> str:
|
| 7 |
|
| 8 |
salt = bcrypt.gensalt()
|
| 9 |
-
hashed_password = bcrypt.hashpw(password.encode(
|
| 10 |
-
return hashed_password.decode(
|
| 11 |
|
| 12 |
@staticmethod
|
| 13 |
def compare_password(stored_password: str, provided_password: str) -> bool:
|
| 14 |
|
| 15 |
-
stored_password = stored_password.encode(
|
| 16 |
-
provided_password = provided_password.encode(
|
| 17 |
return bcrypt.checkpw(provided_password, stored_password)
|
|
|
|
| 6 |
def hash_password(password: str) -> str:
|
| 7 |
|
| 8 |
salt = bcrypt.gensalt()
|
| 9 |
+
hashed_password = bcrypt.hashpw(password.encode("utf-8"), salt)
|
| 10 |
+
return hashed_password.decode("utf-8")
|
| 11 |
|
| 12 |
@staticmethod
|
| 13 |
def compare_password(stored_password: str, provided_password: str) -> bool:
|
| 14 |
|
| 15 |
+
stored_password = stored_password.encode("utf-8")
|
| 16 |
+
provided_password = provided_password.encode("utf-8")
|
| 17 |
return bcrypt.checkpw(provided_password, stored_password)
|
src/utils/_jwt_util.py
CHANGED
|
@@ -2,19 +2,20 @@ import os
|
|
| 2 |
import jwt
|
| 3 |
from datetime import datetime, timedelta
|
| 4 |
|
|
|
|
| 5 |
class JWTUtil:
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
| 2 |
import jwt
|
| 3 |
from datetime import datetime, timedelta
|
| 4 |
|
| 5 |
+
|
| 6 |
class JWTUtil:
|
| 7 |
+
def __init__(self, algorithm="HS256"):
|
| 8 |
+
self.secret_key = os.getenv("JWT_SECRET_KEY")
|
| 9 |
+
self.algorithm = algorithm
|
| 10 |
|
| 11 |
+
def generate_jwt(self, payload, expiration_minutes=60):
|
| 12 |
+
payload["exp"] = datetime.now() + timedelta(minutes=expiration_minutes)
|
| 13 |
+
return jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
|
| 14 |
|
| 15 |
+
def validate_jwt(self, token):
|
| 16 |
+
try:
|
| 17 |
+
return jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
|
| 18 |
+
except jwt.ExpiredSignatureError:
|
| 19 |
+
return None
|
| 20 |
+
except jwt.InvalidTokenError:
|
| 21 |
+
return None
|
src/utils/_openai_client.py
CHANGED
|
@@ -5,102 +5,17 @@ from aiortc import RTCPeerConnection, RTCSessionDescription
|
|
| 5 |
from openai import AsyncOpenAI
|
| 6 |
|
| 7 |
from src.config import logger
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
_OPENAI_TOOLS = [
|
| 11 |
-
{
|
| 12 |
-
"type": "function",
|
| 13 |
-
"name": "get_relevant_information",
|
| 14 |
-
"description": "Get relevant information(Context) to better respond to user queries",
|
| 15 |
-
"parameters": {
|
| 16 |
-
"type": "object",
|
| 17 |
-
"properties": {
|
| 18 |
-
"query": {
|
| 19 |
-
"type": "string",
|
| 20 |
-
"description": "The user query to get relevant information for",
|
| 21 |
-
},
|
| 22 |
-
},
|
| 23 |
-
"required": ["query"],
|
| 24 |
-
},
|
| 25 |
-
},
|
| 26 |
-
{
|
| 27 |
-
"type": "function",
|
| 28 |
-
"name": "get_available_salesperson_meetings",
|
| 29 |
-
"description": "Get all the salespersons ids that can be used to check available slots and to schedule meetings",
|
| 30 |
-
},
|
| 31 |
-
{
|
| 32 |
-
"type": "function",
|
| 33 |
-
"name": "get_available_slots",
|
| 34 |
-
"description": "Get available slots for a salesperson meeting",
|
| 35 |
-
"parameters": {
|
| 36 |
-
"type": "object",
|
| 37 |
-
"properties": {
|
| 38 |
-
"salesperson_meeting_id": {
|
| 39 |
-
"type": "string",
|
| 40 |
-
"description": "The ID of the salesperson meeting",
|
| 41 |
-
},
|
| 42 |
-
"timezone": {
|
| 43 |
-
"type": "string",
|
| 44 |
-
"description": "The timezone for the available slots. It must be a tz identifier like America/New_York, Asia/Kolkata, Cuba, etc.Note: We have regional calenders for APAC, EMEA, and North America. Please use the timezone accordingly. The region must be asked explicitly from the user and hence confirmed too.",
|
| 45 |
-
},
|
| 46 |
-
},
|
| 47 |
-
"required": ["salesperson_meeting_id", "timezone"],
|
| 48 |
-
},
|
| 49 |
-
},
|
| 50 |
-
{
|
| 51 |
-
"type": "function",
|
| 52 |
-
"name": "schedule_meeting",
|
| 53 |
-
"description": "Schedule a meeting",
|
| 54 |
-
"parameters": {
|
| 55 |
-
"type": "object",
|
| 56 |
-
"properties": {
|
| 57 |
-
"user_id": {
|
| 58 |
-
"type": "string",
|
| 59 |
-
"description": "The ID of the user, default: DEFAULT_USER, Do not explicitly ask for this",
|
| 60 |
-
"default": "DEFAULT_USER",
|
| 61 |
-
},
|
| 62 |
-
"salesperson_meeting_id": {
|
| 63 |
-
"type": "string",
|
| 64 |
-
"description": "The ID of the salesperson meeting, this can be obtained from get_available_salesperson_meetings",
|
| 65 |
-
},
|
| 66 |
-
"preferred_start_time": {
|
| 67 |
-
"type": "string",
|
| 68 |
-
"format": "date-time",
|
| 69 |
-
"description": "The preferred start time for the meeting. format: YYYY-MM-DD HH:MM:SS, this can be obtained from get_available_slots",
|
| 70 |
-
"example": "2022-12-17 12:00:00",
|
| 71 |
-
},
|
| 72 |
-
"duration": {
|
| 73 |
-
"type": "integer",
|
| 74 |
-
"description": "The duration of the meeting in minutes",
|
| 75 |
-
},
|
| 76 |
-
"timezone": {
|
| 77 |
-
"type": "string",
|
| 78 |
-
"description": "The timezone for the available slots. It must be a tz identifier like America/New_York, Asia/Kolkata, Cuba, etc.Note: We have regional calenders for APAC, EMEA, and North America. Please use the timezone accordingly. The region must be asked explicitly from the user and hence confirmed too.",
|
| 79 |
-
},
|
| 80 |
-
"locale": {
|
| 81 |
-
"type": "string",
|
| 82 |
-
"description": "The locale for the meeting",
|
| 83 |
-
"default": "en-us",
|
| 84 |
-
},
|
| 85 |
-
},
|
| 86 |
-
"required": [
|
| 87 |
-
"user_id",
|
| 88 |
-
"salesperson_meeting_id",
|
| 89 |
-
"preferred_start_time",
|
| 90 |
-
"duration",
|
| 91 |
-
"timezone",
|
| 92 |
-
],
|
| 93 |
-
},
|
| 94 |
-
},
|
| 95 |
-
]
|
| 96 |
|
| 97 |
|
| 98 |
class OpenAIClient:
|
| 99 |
def __init__(self):
|
| 100 |
self.client = None
|
| 101 |
-
self.
|
| 102 |
-
self.
|
| 103 |
-
self.webrtc_url =
|
|
|
|
|
|
|
| 104 |
self.modalities = ["text", "audio"]
|
| 105 |
self.voice = "alloy"
|
| 106 |
self.transcription_model = "whisper-1"
|
|
@@ -126,7 +41,7 @@ class OpenAIClient:
|
|
| 126 |
|
| 127 |
async def text_generation(self, query: str):
|
| 128 |
completion = await self.client.chat.completions.create(
|
| 129 |
-
model="
|
| 130 |
messages=[
|
| 131 |
{
|
| 132 |
"role": "user",
|
|
|
|
| 5 |
from openai import AsyncOpenAI
|
| 6 |
|
| 7 |
from src.config import logger
|
| 8 |
+
from ._openai_tools import _OPENAI_TOOLS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
|
| 11 |
class OpenAIClient:
|
| 12 |
def __init__(self):
|
| 13 |
self.client = None
|
| 14 |
+
self.model = os.getenv("OPENAI_REALTIME_MODEL")
|
| 15 |
+
self.session_url = os.getenv("OPENAI_BASE_URL") + "/v1/realtime/sessions"
|
| 16 |
+
self.webrtc_url = (
|
| 17 |
+
os.getenv("OPENAI_BASE_URL") + "/v1/realtime?model={self.model}"
|
| 18 |
+
)
|
| 19 |
self.modalities = ["text", "audio"]
|
| 20 |
self.voice = "alloy"
|
| 21 |
self.transcription_model = "whisper-1"
|
|
|
|
| 41 |
|
| 42 |
async def text_generation(self, query: str):
|
| 43 |
completion = await self.client.chat.completions.create(
|
| 44 |
+
model=os.getenv("OPENAI_CHAT_COMPLETION_MODEL"),
|
| 45 |
messages=[
|
| 46 |
{
|
| 47 |
"role": "user",
|
src/utils/_openai_tools.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
_OPENAI_TOOLS = [
|
| 2 |
+
{
|
| 3 |
+
"type": "function",
|
| 4 |
+
"name": "get_relevant_information",
|
| 5 |
+
"description": "Get relevant information(Context) to better respond to user queries",
|
| 6 |
+
"parameters": {
|
| 7 |
+
"type": "object",
|
| 8 |
+
"properties": {
|
| 9 |
+
"query": {
|
| 10 |
+
"type": "string",
|
| 11 |
+
"description": "The user query to get relevant information for",
|
| 12 |
+
},
|
| 13 |
+
},
|
| 14 |
+
"required": ["query"],
|
| 15 |
+
},
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"type": "function",
|
| 19 |
+
"name": "get_available_salesperson_meetings",
|
| 20 |
+
"description": "Get all the salespersons ids that can be used to check available slots and to schedule meetings",
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"type": "function",
|
| 24 |
+
"name": "get_available_slots",
|
| 25 |
+
"description": "Get available slots for a salesperson meeting",
|
| 26 |
+
"parameters": {
|
| 27 |
+
"type": "object",
|
| 28 |
+
"properties": {
|
| 29 |
+
"salesperson_meeting_id": {
|
| 30 |
+
"type": "string",
|
| 31 |
+
"description": "The ID of the salesperson meeting",
|
| 32 |
+
},
|
| 33 |
+
"timezone": {
|
| 34 |
+
"type": "string",
|
| 35 |
+
"description": "The timezone for the available slots. It must be a tz identifier like America/New_York, Asia/Kolkata, Cuba, etc.Note: We have regional calenders for APAC, EMEA, and North America. Please use the timezone accordingly. The region must be asked explicitly from the user and hence confirmed too.",
|
| 36 |
+
},
|
| 37 |
+
},
|
| 38 |
+
"required": ["salesperson_meeting_id", "timezone"],
|
| 39 |
+
},
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
"type": "function",
|
| 43 |
+
"name": "schedule_meeting",
|
| 44 |
+
"description": "Schedule a meeting",
|
| 45 |
+
"parameters": {
|
| 46 |
+
"type": "object",
|
| 47 |
+
"properties": {
|
| 48 |
+
"user_id": {
|
| 49 |
+
"type": "string",
|
| 50 |
+
"description": "The ID of the user, default: DEFAULT_USER, Do not explicitly ask for this",
|
| 51 |
+
"default": "DEFAULT_USER",
|
| 52 |
+
},
|
| 53 |
+
"salesperson_meeting_id": {
|
| 54 |
+
"type": "string",
|
| 55 |
+
"description": "The ID of the salesperson meeting, this can be obtained from get_available_salesperson_meetings",
|
| 56 |
+
},
|
| 57 |
+
"preferred_start_time": {
|
| 58 |
+
"type": "string",
|
| 59 |
+
"format": "date-time",
|
| 60 |
+
"description": "The preferred start time for the meeting. format: YYYY-MM-DD HH:MM:SS, this can be obtained from get_available_slots",
|
| 61 |
+
"example": "2022-12-17 12:00:00",
|
| 62 |
+
},
|
| 63 |
+
"duration": {
|
| 64 |
+
"type": "integer",
|
| 65 |
+
"description": "The duration of the meeting in minutes",
|
| 66 |
+
},
|
| 67 |
+
"timezone": {
|
| 68 |
+
"type": "string",
|
| 69 |
+
"description": "The timezone for the available slots. It must be a tz identifier like America/New_York, Asia/Kolkata, Cuba, etc.Note: We have regional calenders for APAC, EMEA, and North America. Please use the timezone accordingly. The region must be asked explicitly from the user and hence confirmed too.",
|
| 70 |
+
},
|
| 71 |
+
"locale": {
|
| 72 |
+
"type": "string",
|
| 73 |
+
"description": "The locale for the meeting",
|
| 74 |
+
"default": "en-us",
|
| 75 |
+
},
|
| 76 |
+
},
|
| 77 |
+
"required": [
|
| 78 |
+
"user_id",
|
| 79 |
+
"salesperson_meeting_id",
|
| 80 |
+
"preferred_start_time",
|
| 81 |
+
"duration",
|
| 82 |
+
"timezone",
|
| 83 |
+
],
|
| 84 |
+
},
|
| 85 |
+
},
|
| 86 |
+
]
|
src/{config/_redis.py → utils/_redis_client.py}
RENAMED
|
@@ -2,20 +2,13 @@ import os
|
|
| 2 |
import redis.asyncio as redis
|
| 3 |
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
class RedisConfig:
|
| 9 |
def __init__(self):
|
| 10 |
self.redis_url = os.getenv("REDIS_URI")
|
|
|
|
| 11 |
|
| 12 |
def init_redis(self):
|
| 13 |
redis_client = redis.from_url(
|
| 14 |
-
url=self.redis_url,
|
| 15 |
-
encoding="utf-8",
|
| 16 |
-
decode_responses=True,
|
| 17 |
)
|
| 18 |
return redis_client
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
redis_client = RedisConfig().init_redis()
|
|
|
|
| 2 |
import redis.asyncio as redis
|
| 3 |
|
| 4 |
|
| 5 |
+
class RedisClient:
|
|
|
|
|
|
|
|
|
|
| 6 |
def __init__(self):
|
| 7 |
self.redis_url = os.getenv("REDIS_URI")
|
| 8 |
+
self.client = self.init_redis()
|
| 9 |
|
| 10 |
def init_redis(self):
|
| 11 |
redis_client = redis.from_url(
|
| 12 |
+
url=self.redis_url, encoding="utf-8", decode_responses=True
|
|
|
|
|
|
|
| 13 |
)
|
| 14 |
return redis_client
|
|
|
|
|
|
|
|
|