Spaces:
Sleeping
Sleeping
Ajit Panday commited on
Commit ·
7d72bcf
1
Parent(s): 36a486d
Update dependencies and Hugging Face configuration
Browse files- alembic.ini +100 -0
- app/api/calls.py +102 -0
- app/call_processor.py +60 -0
- app/customer_db.py +126 -0
- app/migrations/env.py +84 -0
- app/migrations/versions/add_webhook_url.py +23 -0
- app/models.py +19 -1
- app/webhook_manager.py +54 -0
- customer_webhook/README.md +101 -0
- customer_webhook/config/config.example.php +25 -0
- customer_webhook/examples/apache.conf +1 -0
- customer_webhook/examples/nginx.conf +1 -0
- customer_webhook/src/Database.php +115 -0
- customer_webhook/src/WebhookHandler.php +1 -0
- customer_webhook/src/custom_processor.php +1 -0
- customer_webhook/src/webhook.php +50 -0
- huggingface.yml +3 -1
- requirements.txt +3 -1
alembic.ini
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# A generic, single database configuration.
|
| 2 |
+
|
| 3 |
+
[alembic]
|
| 4 |
+
# path to migration scripts
|
| 5 |
+
script_location = app/migrations
|
| 6 |
+
|
| 7 |
+
# template used to generate migration files
|
| 8 |
+
# file_template = %%(rev)s_%%(slug)s
|
| 9 |
+
|
| 10 |
+
# sys.path path, will be prepended to sys.path if present.
|
| 11 |
+
# defaults to the current working directory.
|
| 12 |
+
prepend_sys_path = .
|
| 13 |
+
|
| 14 |
+
# timezone to use when rendering the date within the migration file
|
| 15 |
+
# as well as the filename.
|
| 16 |
+
# If specified, requires the python-dateutil library that can be
|
| 17 |
+
# installed by adding `alembic[tz]` to the pip requirements
|
| 18 |
+
# timezone =
|
| 19 |
+
|
| 20 |
+
# max length of characters to apply to the
|
| 21 |
+
# "slug" field
|
| 22 |
+
#truncate_slug_length = 40
|
| 23 |
+
|
| 24 |
+
# set to 'true' to run the environment during
|
| 25 |
+
# the 'revision' command, regardless of autogenerate
|
| 26 |
+
# revision_environment = false
|
| 27 |
+
|
| 28 |
+
# set to 'true' to allow .pyc and .pyo files without
|
| 29 |
+
# a source .py file to be detected as revisions in the
|
| 30 |
+
# versions/ directory
|
| 31 |
+
# sourceless = false
|
| 32 |
+
|
| 33 |
+
# version location specification; This defaults
|
| 34 |
+
# to app/migrations/versions. When using multiple version
|
| 35 |
+
# directories, initial revisions must be specified with --version-path.
|
| 36 |
+
# The path separator used here should be the separator specified by "version_path_separator" below.
|
| 37 |
+
# version_locations = %(here)s/bar:%(here)s/bat:app/migrations/versions
|
| 38 |
+
|
| 39 |
+
# version path separator; As mentioned above, this is the character used to split
|
| 40 |
+
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
| 41 |
+
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or colons.
|
| 42 |
+
# Valid values for version_path_separator are:
|
| 43 |
+
#
|
| 44 |
+
# version_path_separator = :
|
| 45 |
+
# version_path_separator = ;
|
| 46 |
+
# version_path_separator = space
|
| 47 |
+
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
| 48 |
+
|
| 49 |
+
# the output encoding used when revision files
|
| 50 |
+
# are written from script.py.mako
|
| 51 |
+
# output_encoding = utf-8
|
| 52 |
+
|
| 53 |
+
sqlalchemy.url = driver://user:pass@localhost/dbname
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
[post_write_hooks]
|
| 57 |
+
# post_write_hooks defines scripts or Python functions that are run
|
| 58 |
+
# on newly generated revision scripts. See the documentation for further
|
| 59 |
+
# detail and examples
|
| 60 |
+
|
| 61 |
+
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
| 62 |
+
# hooks = black
|
| 63 |
+
# black.type = console_scripts
|
| 64 |
+
# black.entrypoint = black
|
| 65 |
+
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
| 66 |
+
|
| 67 |
+
# Logging configuration
|
| 68 |
+
[loggers]
|
| 69 |
+
keys = root,sqlalchemy,alembic
|
| 70 |
+
|
| 71 |
+
[handlers]
|
| 72 |
+
keys = console
|
| 73 |
+
|
| 74 |
+
[formatters]
|
| 75 |
+
keys = generic
|
| 76 |
+
|
| 77 |
+
[logger_root]
|
| 78 |
+
level = WARN
|
| 79 |
+
handlers = console
|
| 80 |
+
qualname =
|
| 81 |
+
|
| 82 |
+
[logger_sqlalchemy]
|
| 83 |
+
level = WARN
|
| 84 |
+
handlers =
|
| 85 |
+
qualname = sqlalchemy.engine
|
| 86 |
+
|
| 87 |
+
[logger_alembic]
|
| 88 |
+
level = INFO
|
| 89 |
+
handlers =
|
| 90 |
+
qualname = alembic
|
| 91 |
+
|
| 92 |
+
[handler_console]
|
| 93 |
+
class = StreamHandler
|
| 94 |
+
args = (sys.stderr,)
|
| 95 |
+
level = NOTSET
|
| 96 |
+
formatter = generic
|
| 97 |
+
|
| 98 |
+
[formatter_generic]
|
| 99 |
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
| 100 |
+
datefmt = %H:%M:%S
|
app/api/calls.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Header
|
| 2 |
+
from typing import Optional
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from ..models import Customer
|
| 5 |
+
from ..call_processor import CallProcessor
|
| 6 |
+
from ..auth import get_current_customer
|
| 7 |
+
import logging
|
| 8 |
+
|
| 9 |
+
router = APIRouter()
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
@router.post("/process-call")
|
| 13 |
+
async def process_call(
|
| 14 |
+
file: UploadFile = File(...),
|
| 15 |
+
caller_number: str = Header(...),
|
| 16 |
+
called_number: str = Header(...),
|
| 17 |
+
customer: Customer = Depends(get_current_customer)
|
| 18 |
+
):
|
| 19 |
+
"""Process a call and send results to customer's webhook"""
|
| 20 |
+
try:
|
| 21 |
+
# Create call processor for the customer
|
| 22 |
+
processor = CallProcessor(customer)
|
| 23 |
+
|
| 24 |
+
# Process and send the call
|
| 25 |
+
result = processor.process_and_send_call(file, caller_number, called_number)
|
| 26 |
+
|
| 27 |
+
if not result:
|
| 28 |
+
raise HTTPException(
|
| 29 |
+
status_code=500,
|
| 30 |
+
detail="Failed to send call results to webhook"
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
return result
|
| 34 |
+
|
| 35 |
+
except Exception as e:
|
| 36 |
+
logger.error(f"Failed to process call: {str(e)}")
|
| 37 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 38 |
+
|
| 39 |
+
@router.post("/webhook")
|
| 40 |
+
async def update_webhook(
|
| 41 |
+
webhook_url: str,
|
| 42 |
+
customer: Customer = Depends(get_current_customer)
|
| 43 |
+
):
|
| 44 |
+
"""Update customer's webhook URL"""
|
| 45 |
+
try:
|
| 46 |
+
# Update webhook URL
|
| 47 |
+
customer.webhook_url = webhook_url
|
| 48 |
+
customer.save()
|
| 49 |
+
|
| 50 |
+
return {"message": "Webhook URL updated successfully"}
|
| 51 |
+
|
| 52 |
+
except Exception as e:
|
| 53 |
+
logger.error(f"Failed to update webhook URL: {str(e)}")
|
| 54 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 55 |
+
|
| 56 |
+
@router.get("/calls/{call_id}")
|
| 57 |
+
async def get_call(
|
| 58 |
+
call_id: str,
|
| 59 |
+
customer: Customer = Depends(get_current_customer)
|
| 60 |
+
):
|
| 61 |
+
"""Get call details from customer's database"""
|
| 62 |
+
try:
|
| 63 |
+
processor = CallProcessor(customer)
|
| 64 |
+
call_data = processor.get_call_details(call_id)
|
| 65 |
+
|
| 66 |
+
if not call_data:
|
| 67 |
+
raise HTTPException(status_code=404, detail="Call not found")
|
| 68 |
+
|
| 69 |
+
return call_data
|
| 70 |
+
|
| 71 |
+
except Exception as e:
|
| 72 |
+
logger.error(f"Failed to get call details: {str(e)}")
|
| 73 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 74 |
+
|
| 75 |
+
@router.get("/calls/search")
|
| 76 |
+
async def search_calls(
|
| 77 |
+
start_date: Optional[datetime] = None,
|
| 78 |
+
end_date: Optional[datetime] = None,
|
| 79 |
+
caller_number: Optional[str] = None,
|
| 80 |
+
called_number: Optional[str] = None,
|
| 81 |
+
customer: Customer = Depends(get_current_customer)
|
| 82 |
+
):
|
| 83 |
+
"""Search calls in customer's database"""
|
| 84 |
+
try:
|
| 85 |
+
processor = CallProcessor(customer)
|
| 86 |
+
|
| 87 |
+
filters = {
|
| 88 |
+
'start_date': start_date,
|
| 89 |
+
'end_date': end_date,
|
| 90 |
+
'caller_number': caller_number,
|
| 91 |
+
'called_number': called_number
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
results = processor.search_calls(filters)
|
| 95 |
+
return {
|
| 96 |
+
'calls': results,
|
| 97 |
+
'total': len(results)
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
except Exception as e:
|
| 101 |
+
logger.error(f"Failed to search calls: {str(e)}")
|
| 102 |
+
raise HTTPException(status_code=500, detail=str(e))
|
app/call_processor.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .webhook_manager import WebhookManager
|
| 2 |
+
import logging
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
import uuid
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
class CallProcessor:
|
| 9 |
+
def __init__(self, customer):
|
| 10 |
+
self.customer = customer
|
| 11 |
+
self.webhook_manager = WebhookManager(customer)
|
| 12 |
+
|
| 13 |
+
def process_and_send_call(self, audio_file, caller_number, called_number):
|
| 14 |
+
"""Process call and send results to customer's webhook"""
|
| 15 |
+
try:
|
| 16 |
+
# Generate a unique call ID
|
| 17 |
+
call_id = str(uuid.uuid4())
|
| 18 |
+
|
| 19 |
+
# Process the call (transcription, summary, sentiment)
|
| 20 |
+
# This is where you would integrate with your existing call processing logic
|
| 21 |
+
# For now, we'll use placeholder data
|
| 22 |
+
call_data = {
|
| 23 |
+
'id': call_id,
|
| 24 |
+
'caller_number': caller_number,
|
| 25 |
+
'called_number': called_number,
|
| 26 |
+
'transcription': "Sample transcription...", # Replace with actual transcription
|
| 27 |
+
'summary': "Sample summary...", # Replace with actual summary
|
| 28 |
+
'sentiment': "positive", # Replace with actual sentiment
|
| 29 |
+
'keywords': "sample, keywords" # Replace with actual keywords
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
# Send to customer's webhook
|
| 33 |
+
success = self.webhook_manager.send_call_results(call_data)
|
| 34 |
+
|
| 35 |
+
if success:
|
| 36 |
+
logger.info(f"Successfully processed and sent call {call_id} for customer {self.customer.id}")
|
| 37 |
+
return call_data
|
| 38 |
+
else:
|
| 39 |
+
logger.warning(f"Failed to send call results to webhook for customer {self.customer.id}")
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
except Exception as e:
|
| 43 |
+
logger.error(f"Failed to process and send call for customer {self.customer.id}: {str(e)}")
|
| 44 |
+
raise
|
| 45 |
+
|
| 46 |
+
def get_call_details(self, call_id):
|
| 47 |
+
"""Get call details from customer's database"""
|
| 48 |
+
try:
|
| 49 |
+
return self.db_manager.get_call_record(call_id)
|
| 50 |
+
except Exception as e:
|
| 51 |
+
logger.error(f"Failed to get call details for call {call_id}: {str(e)}")
|
| 52 |
+
raise
|
| 53 |
+
|
| 54 |
+
def search_calls(self, filters=None):
|
| 55 |
+
"""Search calls in customer's database"""
|
| 56 |
+
try:
|
| 57 |
+
return self.db_manager.search_call_records(filters)
|
| 58 |
+
except Exception as e:
|
| 59 |
+
logger.error(f"Failed to search calls for customer {self.customer.id}: {str(e)}")
|
| 60 |
+
raise
|
app/customer_db.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import create_engine, text
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
import logging
|
| 4 |
+
|
| 5 |
+
logger = logging.getLogger(__name__)
|
| 6 |
+
|
| 7 |
+
class CustomerDBManager:
|
| 8 |
+
def __init__(self, customer):
|
| 9 |
+
self.customer = customer
|
| 10 |
+
self.engine = None
|
| 11 |
+
|
| 12 |
+
def get_connection(self):
|
| 13 |
+
"""Get database connection for the customer"""
|
| 14 |
+
if not self.engine:
|
| 15 |
+
try:
|
| 16 |
+
# Create database URL from customer's database credentials
|
| 17 |
+
db_url = f"mysql+pymysql://{self.customer.db_user}:{self.customer.db_password}@{self.customer.db_host}:{self.customer.db_port}/{self.customer.db_name}"
|
| 18 |
+
self.engine = create_engine(db_url)
|
| 19 |
+
except Exception as e:
|
| 20 |
+
logger.error(f"Failed to create database connection for customer {self.customer.id}: {str(e)}")
|
| 21 |
+
raise
|
| 22 |
+
return self.engine
|
| 23 |
+
|
| 24 |
+
def save_call_record(self, call_data):
|
| 25 |
+
"""Save call record to customer's database"""
|
| 26 |
+
try:
|
| 27 |
+
engine = self.get_connection()
|
| 28 |
+
|
| 29 |
+
# Prepare the SQL query
|
| 30 |
+
query = text("""
|
| 31 |
+
INSERT INTO call_records (
|
| 32 |
+
id, customer_id, caller_number, called_number,
|
| 33 |
+
transcription, summary, sentiment, keywords,
|
| 34 |
+
created_at, updated_at
|
| 35 |
+
) VALUES (
|
| 36 |
+
:id, :customer_id, :caller_number, :called_number,
|
| 37 |
+
:transcription, :summary, :sentiment, :keywords,
|
| 38 |
+
:created_at, :updated_at
|
| 39 |
+
)
|
| 40 |
+
""")
|
| 41 |
+
|
| 42 |
+
# Prepare the data
|
| 43 |
+
now = datetime.utcnow()
|
| 44 |
+
record_data = {
|
| 45 |
+
'id': call_data.get('id'),
|
| 46 |
+
'customer_id': self.customer.id,
|
| 47 |
+
'caller_number': call_data.get('caller_number'),
|
| 48 |
+
'called_number': call_data.get('called_number'),
|
| 49 |
+
'transcription': call_data.get('transcription'),
|
| 50 |
+
'summary': call_data.get('summary'),
|
| 51 |
+
'sentiment': call_data.get('sentiment'),
|
| 52 |
+
'keywords': call_data.get('keywords'),
|
| 53 |
+
'created_at': now,
|
| 54 |
+
'updated_at': now
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
# Execute the query
|
| 58 |
+
with engine.connect() as connection:
|
| 59 |
+
connection.execute(query, record_data)
|
| 60 |
+
connection.commit()
|
| 61 |
+
|
| 62 |
+
logger.info(f"Successfully saved call record {call_data.get('id')} for customer {self.customer.id}")
|
| 63 |
+
return True
|
| 64 |
+
|
| 65 |
+
except Exception as e:
|
| 66 |
+
logger.error(f"Failed to save call record for customer {self.customer.id}: {str(e)}")
|
| 67 |
+
raise
|
| 68 |
+
|
| 69 |
+
def get_call_record(self, call_id):
|
| 70 |
+
"""Retrieve a call record from customer's database"""
|
| 71 |
+
try:
|
| 72 |
+
engine = self.get_connection()
|
| 73 |
+
|
| 74 |
+
query = text("""
|
| 75 |
+
SELECT * FROM call_records
|
| 76 |
+
WHERE id = :call_id AND customer_id = :customer_id
|
| 77 |
+
""")
|
| 78 |
+
|
| 79 |
+
with engine.connect() as connection:
|
| 80 |
+
result = connection.execute(query, {
|
| 81 |
+
'call_id': call_id,
|
| 82 |
+
'customer_id': self.customer.id
|
| 83 |
+
}).fetchone()
|
| 84 |
+
|
| 85 |
+
return dict(result) if result else None
|
| 86 |
+
|
| 87 |
+
except Exception as e:
|
| 88 |
+
logger.error(f"Failed to retrieve call record {call_id} for customer {self.customer.id}: {str(e)}")
|
| 89 |
+
raise
|
| 90 |
+
|
| 91 |
+
def search_call_records(self, filters=None):
|
| 92 |
+
"""Search call records in customer's database"""
|
| 93 |
+
try:
|
| 94 |
+
engine = self.get_connection()
|
| 95 |
+
|
| 96 |
+
# Build the query based on filters
|
| 97 |
+
query = text("""
|
| 98 |
+
SELECT * FROM call_records
|
| 99 |
+
WHERE customer_id = :customer_id
|
| 100 |
+
""")
|
| 101 |
+
params = {'customer_id': self.customer.id}
|
| 102 |
+
|
| 103 |
+
if filters:
|
| 104 |
+
if filters.get('start_date'):
|
| 105 |
+
query = query.text + " AND created_at >= :start_date"
|
| 106 |
+
params['start_date'] = filters['start_date']
|
| 107 |
+
if filters.get('end_date'):
|
| 108 |
+
query = query.text + " AND created_at <= :end_date"
|
| 109 |
+
params['end_date'] = filters['end_date']
|
| 110 |
+
if filters.get('caller_number'):
|
| 111 |
+
query = query.text + " AND caller_number = :caller_number"
|
| 112 |
+
params['caller_number'] = filters['caller_number']
|
| 113 |
+
if filters.get('called_number'):
|
| 114 |
+
query = query.text + " AND called_number = :called_number"
|
| 115 |
+
params['called_number'] = filters['called_number']
|
| 116 |
+
|
| 117 |
+
query = query.text + " ORDER BY created_at DESC"
|
| 118 |
+
|
| 119 |
+
with engine.connect() as connection:
|
| 120 |
+
results = connection.execute(query, params).fetchall()
|
| 121 |
+
|
| 122 |
+
return [dict(row) for row in results]
|
| 123 |
+
|
| 124 |
+
except Exception as e:
|
| 125 |
+
logger.error(f"Failed to search call records for customer {self.customer.id}: {str(e)}")
|
| 126 |
+
raise
|
app/migrations/env.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from logging.config import fileConfig
|
| 2 |
+
|
| 3 |
+
from sqlalchemy import engine_from_config
|
| 4 |
+
from sqlalchemy import pool
|
| 5 |
+
|
| 6 |
+
from alembic import context
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
| 11 |
+
|
| 12 |
+
from app.models import Base
|
| 13 |
+
from app.database import get_db_url
|
| 14 |
+
|
| 15 |
+
# this is the Alembic Config object, which provides
|
| 16 |
+
# access to the values within the .ini file in use.
|
| 17 |
+
config = context.config
|
| 18 |
+
|
| 19 |
+
# Interpret the config file for Python logging.
|
| 20 |
+
# This line sets up loggers basically.
|
| 21 |
+
if config.config_file_name is not None:
|
| 22 |
+
fileConfig(config.config_file_name)
|
| 23 |
+
|
| 24 |
+
# add your model's MetaData object here
|
| 25 |
+
# for 'autogenerate' support
|
| 26 |
+
target_metadata = Base.metadata
|
| 27 |
+
|
| 28 |
+
# other values from the config, defined by the needs of env.py,
|
| 29 |
+
# can be acquired:
|
| 30 |
+
# my_important_option = config.get_main_option("my_important_option")
|
| 31 |
+
# ... etc.
|
| 32 |
+
|
| 33 |
+
def run_migrations_offline() -> None:
|
| 34 |
+
"""Run migrations in 'offline' mode.
|
| 35 |
+
|
| 36 |
+
This configures the context with just a URL
|
| 37 |
+
and not an Engine, though an Engine is acceptable
|
| 38 |
+
here as well. By skipping the Engine creation
|
| 39 |
+
we don't even need a DBAPI to be available.
|
| 40 |
+
|
| 41 |
+
Calls to context.execute() here emit the given string to the
|
| 42 |
+
script output.
|
| 43 |
+
|
| 44 |
+
"""
|
| 45 |
+
url = get_db_url()
|
| 46 |
+
context.configure(
|
| 47 |
+
url=url,
|
| 48 |
+
target_metadata=target_metadata,
|
| 49 |
+
literal_binds=True,
|
| 50 |
+
dialect_opts={"paramstyle": "named"},
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
with context.begin_transaction():
|
| 54 |
+
context.run_migrations()
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def run_migrations_online() -> None:
|
| 58 |
+
"""Run migrations in 'online' mode.
|
| 59 |
+
|
| 60 |
+
In this scenario we need to create an Engine
|
| 61 |
+
and associate a connection with the context.
|
| 62 |
+
|
| 63 |
+
"""
|
| 64 |
+
configuration = config.get_section(config.config_ini_section)
|
| 65 |
+
configuration["sqlalchemy.url"] = get_db_url()
|
| 66 |
+
connectable = engine_from_config(
|
| 67 |
+
configuration,
|
| 68 |
+
prefix="sqlalchemy.",
|
| 69 |
+
poolclass=pool.NullPool,
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
with connectable.connect() as connection:
|
| 73 |
+
context.configure(
|
| 74 |
+
connection=connection, target_metadata=target_metadata
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
with context.begin_transaction():
|
| 78 |
+
context.run_migrations()
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
if context.is_offline_mode():
|
| 82 |
+
run_migrations_offline()
|
| 83 |
+
else:
|
| 84 |
+
run_migrations_online()
|
app/migrations/versions/add_webhook_url.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Add webhook_url column to customers table
|
| 2 |
+
|
| 3 |
+
Revision ID: add_webhook_url
|
| 4 |
+
Revises:
|
| 5 |
+
Create Date: 2024-03-14 12:00:00.000000
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from alembic import op
|
| 9 |
+
import sqlalchemy as sa
|
| 10 |
+
|
| 11 |
+
# revision identifiers, used by Alembic.
|
| 12 |
+
revision = 'add_webhook_url'
|
| 13 |
+
down_revision = None
|
| 14 |
+
branch_labels = None
|
| 15 |
+
depends_on = None
|
| 16 |
+
|
| 17 |
+
def upgrade():
|
| 18 |
+
# Add webhook_url column to customers table
|
| 19 |
+
op.add_column('customers', sa.Column('webhook_url', sa.String(255), nullable=True))
|
| 20 |
+
|
| 21 |
+
def downgrade():
|
| 22 |
+
# Remove webhook_url column from customers table
|
| 23 |
+
op.drop_column('customers', 'webhook_url')
|
app/models.py
CHANGED
|
@@ -20,7 +20,7 @@ class Customer(Base):
|
|
| 20 |
company_name = Column(String(100), nullable=False)
|
| 21 |
email = Column(String(100), unique=True, nullable=False)
|
| 22 |
api_key = Column(String(64), unique=True, nullable=False)
|
| 23 |
-
webhook_url = Column(String(255), nullable=True) # URL where call results will be sent
|
| 24 |
is_active = Column(Boolean, default=True)
|
| 25 |
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
| 26 |
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
@@ -66,6 +66,24 @@ class Customer(Base):
|
|
| 66 |
response.raise_for_status()
|
| 67 |
return response.json()
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
class CallRecord(Base):
|
| 70 |
__tablename__ = "call_records"
|
| 71 |
|
|
|
|
| 20 |
company_name = Column(String(100), nullable=False)
|
| 21 |
email = Column(String(100), unique=True, nullable=False)
|
| 22 |
api_key = Column(String(64), unique=True, nullable=False)
|
| 23 |
+
webhook_url = Column(String(255), nullable=True, default=None) # URL where call results will be sent
|
| 24 |
is_active = Column(Boolean, default=True)
|
| 25 |
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
| 26 |
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
| 66 |
response.raise_for_status()
|
| 67 |
return response.json()
|
| 68 |
|
| 69 |
+
def send_webhook(self, data: Dict) -> bool:
|
| 70 |
+
"""Send call results to webhook URL if configured"""
|
| 71 |
+
if not self.webhook_url:
|
| 72 |
+
return False
|
| 73 |
+
|
| 74 |
+
try:
|
| 75 |
+
response = requests.post(
|
| 76 |
+
self.webhook_url,
|
| 77 |
+
json=data,
|
| 78 |
+
headers={"Content-Type": "application/json"},
|
| 79 |
+
timeout=5
|
| 80 |
+
)
|
| 81 |
+
response.raise_for_status()
|
| 82 |
+
return True
|
| 83 |
+
except Exception as e:
|
| 84 |
+
print(f"Webhook delivery failed: {str(e)}")
|
| 85 |
+
return False
|
| 86 |
+
|
| 87 |
class CallRecord(Base):
|
| 88 |
__tablename__ = "call_records"
|
| 89 |
|
app/webhook_manager.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import logging
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
import json
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
class WebhookManager:
|
| 9 |
+
def __init__(self, customer):
|
| 10 |
+
self.customer = customer
|
| 11 |
+
self.webhook_url = customer.webhook_url
|
| 12 |
+
|
| 13 |
+
def send_call_results(self, call_data):
|
| 14 |
+
"""Send call analysis results to customer's webhook URL"""
|
| 15 |
+
if not self.webhook_url:
|
| 16 |
+
logger.warning(f"No webhook URL configured for customer {self.customer.id}")
|
| 17 |
+
return False
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
# Prepare the payload
|
| 21 |
+
payload = {
|
| 22 |
+
'call_id': call_data.get('id'),
|
| 23 |
+
'caller_number': call_data.get('caller_number'),
|
| 24 |
+
'called_number': call_data.get('called_number'),
|
| 25 |
+
'transcription': call_data.get('transcription'),
|
| 26 |
+
'summary': call_data.get('summary'),
|
| 27 |
+
'sentiment': call_data.get('sentiment'),
|
| 28 |
+
'keywords': call_data.get('keywords'),
|
| 29 |
+
'timestamp': datetime.utcnow().isoformat(),
|
| 30 |
+
'customer_id': self.customer.id
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
# Send POST request to webhook URL
|
| 34 |
+
response = requests.post(
|
| 35 |
+
self.webhook_url,
|
| 36 |
+
json=payload,
|
| 37 |
+
headers={'Content-Type': 'application/json'},
|
| 38 |
+
timeout=10 # 10 seconds timeout
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
# Check if request was successful
|
| 42 |
+
if response.status_code in [200, 201, 202]:
|
| 43 |
+
logger.info(f"Successfully sent call results to webhook for customer {self.customer.id}")
|
| 44 |
+
return True
|
| 45 |
+
else:
|
| 46 |
+
logger.error(f"Failed to send call results to webhook. Status code: {response.status_code}")
|
| 47 |
+
return False
|
| 48 |
+
|
| 49 |
+
except requests.exceptions.RequestException as e:
|
| 50 |
+
logger.error(f"Error sending webhook for customer {self.customer.id}: {str(e)}")
|
| 51 |
+
return False
|
| 52 |
+
except Exception as e:
|
| 53 |
+
logger.error(f"Unexpected error sending webhook for customer {self.customer.id}: {str(e)}")
|
| 54 |
+
return False
|
customer_webhook/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# vBot Customer Webhook Handler
|
| 2 |
+
|
| 3 |
+
This package provides a simple webhook handler for vBot customers to receive and process call analysis results on their own servers.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- Secure webhook endpoint with API key validation
|
| 8 |
+
- Automatic call record storage in MySQL database
|
| 9 |
+
- Configurable webhook secret for additional security
|
| 10 |
+
- Easy integration with LAMP stack
|
| 11 |
+
- Example implementation included
|
| 12 |
+
|
| 13 |
+
## Requirements
|
| 14 |
+
|
| 15 |
+
- PHP 7.4 or higher
|
| 16 |
+
- MySQL 5.7 or higher
|
| 17 |
+
- Apache/Nginx web server
|
| 18 |
+
- mod_rewrite enabled (for Apache)
|
| 19 |
+
|
| 20 |
+
## Installation
|
| 21 |
+
|
| 22 |
+
1. Copy the contents of this package to your web server directory:
|
| 23 |
+
```bash
|
| 24 |
+
cp -r customer_webhook/* /var/www/html/vbot-webhook/
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
2. Create the MySQL database and table:
|
| 28 |
+
```sql
|
| 29 |
+
CREATE DATABASE vbot_calls;
|
| 30 |
+
USE vbot_calls;
|
| 31 |
+
|
| 32 |
+
CREATE TABLE call_records (
|
| 33 |
+
id VARCHAR(36) PRIMARY KEY,
|
| 34 |
+
customer_id INT NOT NULL,
|
| 35 |
+
caller_number VARCHAR(20) NOT NULL,
|
| 36 |
+
called_number VARCHAR(20) NOT NULL,
|
| 37 |
+
transcription TEXT,
|
| 38 |
+
summary TEXT,
|
| 39 |
+
sentiment VARCHAR(50),
|
| 40 |
+
keywords TEXT,
|
| 41 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 42 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
| 43 |
+
);
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
3. Configure the webhook handler:
|
| 47 |
+
- Copy `config/config.example.php` to `config/config.php`
|
| 48 |
+
- Update the database credentials and webhook secret
|
| 49 |
+
|
| 50 |
+
4. Set up Apache/Nginx:
|
| 51 |
+
- Ensure the webhook directory is accessible
|
| 52 |
+
- Configure URL rewriting (see examples)
|
| 53 |
+
|
| 54 |
+
## Configuration
|
| 55 |
+
|
| 56 |
+
Edit `config/config.php` to set your:
|
| 57 |
+
- Database credentials
|
| 58 |
+
- Webhook secret (for additional security)
|
| 59 |
+
- API key (provided by vBot)
|
| 60 |
+
|
| 61 |
+
## Usage
|
| 62 |
+
|
| 63 |
+
1. Your webhook URL will be: `https://your-domain.com/vbot-webhook/webhook.php`
|
| 64 |
+
|
| 65 |
+
2. The webhook will receive POST requests with call analysis data in JSON format:
|
| 66 |
+
```json
|
| 67 |
+
{
|
| 68 |
+
"call_id": "uuid",
|
| 69 |
+
"caller_number": "+1234567890",
|
| 70 |
+
"called_number": "+0987654321",
|
| 71 |
+
"transcription": "Call transcript...",
|
| 72 |
+
"summary": "Call summary...",
|
| 73 |
+
"sentiment": "positive",
|
| 74 |
+
"keywords": "keyword1, keyword2",
|
| 75 |
+
"timestamp": "2024-03-14T12:00:00Z",
|
| 76 |
+
"customer_id": 123
|
| 77 |
+
}
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
3. The handler will:
|
| 81 |
+
- Validate the API key
|
| 82 |
+
- Store the call record in your database
|
| 83 |
+
- Return a success response
|
| 84 |
+
|
| 85 |
+
## Security
|
| 86 |
+
|
| 87 |
+
- All requests must include the API key in the `X-API-Key` header
|
| 88 |
+
- Optional webhook secret for additional security
|
| 89 |
+
- Input validation and sanitization
|
| 90 |
+
- SQL injection prevention
|
| 91 |
+
|
| 92 |
+
## Examples
|
| 93 |
+
|
| 94 |
+
See the `examples` directory for:
|
| 95 |
+
- Apache configuration
|
| 96 |
+
- Nginx configuration
|
| 97 |
+
- Custom processing example
|
| 98 |
+
|
| 99 |
+
## Support
|
| 100 |
+
|
| 101 |
+
For support, contact vBot support team or refer to the documentation.
|
customer_webhook/config/config.example.php
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
// Database configuration
|
| 3 |
+
define('DB_HOST', 'localhost');
|
| 4 |
+
define('DB_NAME', 'vbot_calls');
|
| 5 |
+
define('DB_USER', 'your_db_user');
|
| 6 |
+
define('DB_PASS', 'your_db_password');
|
| 7 |
+
|
| 8 |
+
// vBot API configuration
|
| 9 |
+
define('VBOT_API_KEY', 'your_api_key_here'); // API key provided by vBot
|
| 10 |
+
define('WEBHOOK_SECRET', 'your_webhook_secret_here'); // Optional secret for additional security
|
| 11 |
+
|
| 12 |
+
// Error reporting
|
| 13 |
+
error_reporting(E_ALL);
|
| 14 |
+
ini_set('display_errors', 0); // Set to 1 for development
|
| 15 |
+
ini_set('log_errors', 1);
|
| 16 |
+
ini_set('error_log', __DIR__ . '/../logs/error.log');
|
| 17 |
+
|
| 18 |
+
// Response settings
|
| 19 |
+
define('ALLOW_ORIGIN', '*'); // Set to your domain in production
|
| 20 |
+
define('MAX_RETRIES', 3); // Maximum number of retry attempts
|
| 21 |
+
define('RETRY_DELAY', 5); // Delay between retries in seconds
|
| 22 |
+
|
| 23 |
+
// Custom processing
|
| 24 |
+
define('ENABLE_CUSTOM_PROCESSING', false); // Set to true to enable custom processing
|
| 25 |
+
define('CUSTOM_PROCESSOR_FILE', __DIR__ . '/../src/custom_processor.php');
|
customer_webhook/examples/apache.conf
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
customer_webhook/examples/nginx.conf
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
customer_webhook/src/Database.php
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
class Database {
|
| 3 |
+
private static $instance = null;
|
| 4 |
+
private $connection = null;
|
| 5 |
+
|
| 6 |
+
private function __construct() {
|
| 7 |
+
try {
|
| 8 |
+
$this->connection = new PDO(
|
| 9 |
+
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME,
|
| 10 |
+
DB_USER,
|
| 11 |
+
DB_PASS,
|
| 12 |
+
[
|
| 13 |
+
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
| 14 |
+
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
| 15 |
+
PDO::ATTR_EMULATE_PREPARES => false
|
| 16 |
+
]
|
| 17 |
+
);
|
| 18 |
+
} catch (PDOException $e) {
|
| 19 |
+
error_log("Database Connection Error: " . $e->getMessage());
|
| 20 |
+
throw new Exception("Database connection failed");
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
public static function getInstance() {
|
| 25 |
+
if (self::$instance === null) {
|
| 26 |
+
self::$instance = new self();
|
| 27 |
+
}
|
| 28 |
+
return self::$instance;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
public function getConnection() {
|
| 32 |
+
return $this->connection;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
public function saveCallRecord($data) {
|
| 36 |
+
try {
|
| 37 |
+
$sql = "INSERT INTO call_records (
|
| 38 |
+
id, customer_id, caller_number, called_number,
|
| 39 |
+
transcription, summary, sentiment, keywords,
|
| 40 |
+
created_at, updated_at
|
| 41 |
+
) VALUES (
|
| 42 |
+
:id, :customer_id, :caller_number, :called_number,
|
| 43 |
+
:transcription, :summary, :sentiment, :keywords,
|
| 44 |
+
NOW(), NOW()
|
| 45 |
+
)";
|
| 46 |
+
|
| 47 |
+
$stmt = $this->connection->prepare($sql);
|
| 48 |
+
|
| 49 |
+
return $stmt->execute([
|
| 50 |
+
'id' => $data['call_id'],
|
| 51 |
+
'customer_id' => $data['customer_id'],
|
| 52 |
+
'caller_number' => $data['caller_number'],
|
| 53 |
+
'called_number' => $data['called_number'],
|
| 54 |
+
'transcription' => $data['transcription'],
|
| 55 |
+
'summary' => $data['summary'],
|
| 56 |
+
'sentiment' => $data['sentiment'],
|
| 57 |
+
'keywords' => $data['keywords']
|
| 58 |
+
]);
|
| 59 |
+
|
| 60 |
+
} catch (PDOException $e) {
|
| 61 |
+
error_log("Database Error: " . $e->getMessage());
|
| 62 |
+
throw new Exception("Failed to save call record");
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
public function getCallRecord($callId) {
|
| 67 |
+
try {
|
| 68 |
+
$sql = "SELECT * FROM call_records WHERE id = :id";
|
| 69 |
+
$stmt = $this->connection->prepare($sql);
|
| 70 |
+
$stmt->execute(['id' => $callId]);
|
| 71 |
+
return $stmt->fetch();
|
| 72 |
+
|
| 73 |
+
} catch (PDOException $e) {
|
| 74 |
+
error_log("Database Error: " . $e->getMessage());
|
| 75 |
+
throw new Exception("Failed to retrieve call record");
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
public function searchCallRecords($filters = []) {
|
| 80 |
+
try {
|
| 81 |
+
$sql = "SELECT * FROM call_records WHERE 1=1";
|
| 82 |
+
$params = [];
|
| 83 |
+
|
| 84 |
+
if (!empty($filters['start_date'])) {
|
| 85 |
+
$sql .= " AND created_at >= :start_date";
|
| 86 |
+
$params['start_date'] = $filters['start_date'];
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
if (!empty($filters['end_date'])) {
|
| 90 |
+
$sql .= " AND created_at <= :end_date";
|
| 91 |
+
$params['end_date'] = $filters['end_date'];
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
if (!empty($filters['caller_number'])) {
|
| 95 |
+
$sql .= " AND caller_number = :caller_number";
|
| 96 |
+
$params['caller_number'] = $filters['caller_number'];
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
if (!empty($filters['called_number'])) {
|
| 100 |
+
$sql .= " AND called_number = :called_number";
|
| 101 |
+
$params['called_number'] = $filters['called_number'];
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
$sql .= " ORDER BY created_at DESC";
|
| 105 |
+
|
| 106 |
+
$stmt = $this->connection->prepare($sql);
|
| 107 |
+
$stmt->execute($params);
|
| 108 |
+
return $stmt->fetchAll();
|
| 109 |
+
|
| 110 |
+
} catch (PDOException $e) {
|
| 111 |
+
error_log("Database Error: " . $e->getMessage());
|
| 112 |
+
throw new Exception("Failed to search call records");
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
}
|
customer_webhook/src/WebhookHandler.php
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
customer_webhook/src/custom_processor.php
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
customer_webhook/src/webhook.php
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
require_once __DIR__ . '/../config/config.php';
|
| 3 |
+
require_once __DIR__ . '/Database.php';
|
| 4 |
+
require_once __DIR__ . '/WebhookHandler.php';
|
| 5 |
+
|
| 6 |
+
// Set headers
|
| 7 |
+
header('Content-Type: application/json');
|
| 8 |
+
header('Access-Control-Allow-Origin: ' . ALLOW_ORIGIN);
|
| 9 |
+
header('Access-Control-Allow-Methods: POST');
|
| 10 |
+
header('Access-Control-Allow-Headers: Content-Type, X-API-Key');
|
| 11 |
+
|
| 12 |
+
// Handle preflight requests
|
| 13 |
+
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
| 14 |
+
http_response_code(200);
|
| 15 |
+
exit();
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
// Only allow POST requests
|
| 19 |
+
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
| 20 |
+
http_response_code(405);
|
| 21 |
+
echo json_encode(['error' => 'Method not allowed']);
|
| 22 |
+
exit();
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
try {
|
| 26 |
+
// Initialize webhook handler
|
| 27 |
+
$handler = new WebhookHandler();
|
| 28 |
+
|
| 29 |
+
// Process the webhook
|
| 30 |
+
$result = $handler->process();
|
| 31 |
+
|
| 32 |
+
// Return success response
|
| 33 |
+
http_response_code(200);
|
| 34 |
+
echo json_encode([
|
| 35 |
+
'success' => true,
|
| 36 |
+
'message' => 'Call record processed successfully',
|
| 37 |
+
'call_id' => $result['call_id'] ?? null
|
| 38 |
+
]);
|
| 39 |
+
|
| 40 |
+
} catch (Exception $e) {
|
| 41 |
+
// Log error
|
| 42 |
+
error_log("Webhook Error: " . $e->getMessage());
|
| 43 |
+
|
| 44 |
+
// Return error response
|
| 45 |
+
http_response_code(500);
|
| 46 |
+
echo json_encode([
|
| 47 |
+
'success' => false,
|
| 48 |
+
'error' => 'Internal server error'
|
| 49 |
+
]);
|
| 50 |
+
}
|
huggingface.yml
CHANGED
|
@@ -1 +1,3 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
image: python:3.9
|
| 2 |
+
pip:
|
| 3 |
+
- requirements.txt
|
requirements.txt
CHANGED
|
@@ -12,4 +12,6 @@ transformers==4.37.2
|
|
| 12 |
torch==2.2.0
|
| 13 |
soundfile==0.12.1
|
| 14 |
librosa==0.10.1
|
| 15 |
-
numpy==1.26.3
|
|
|
|
|
|
|
|
|
| 12 |
torch==2.2.0
|
| 13 |
soundfile==0.12.1
|
| 14 |
librosa==0.10.1
|
| 15 |
+
numpy==1.26.3
|
| 16 |
+
alembic==1.13.1
|
| 17 |
+
requests==2.31.0
|