Ajit Panday commited on
Commit
c6d0d33
·
1 Parent(s): 6d5c3a1

Initial commit with complete vBot implementation

Browse files
Files changed (5) hide show
  1. app/auth.py +139 -1
  2. app/models.py +24 -2
  3. asterisk_dialplan.conf +14 -0
  4. main.py +73 -21
  5. requirements.txt +6 -1
app/auth.py CHANGED
@@ -1 +1,139 @@
1
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status
2
+ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
3
+ from sqlalchemy.orm import Session
4
+ from datetime import datetime, timedelta
5
+ from jose import JWTError, jwt
6
+ from passlib.context import CryptContext
7
+ from typing import Optional
8
+ import secrets
9
+ import os
10
+ from dotenv import load_dotenv
11
+ from . import models
12
+
13
+ # Load environment variables
14
+ load_dotenv()
15
+
16
+ # Security configuration
17
+ SECRET_KEY = os.getenv("JWT_SECRET", "your-secret-key")
18
+ ALGORITHM = "HS256"
19
+ ACCESS_TOKEN_EXPIRE_MINUTES = 30
20
+
21
+ # Password hashing
22
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
23
+
24
+ # OAuth2 scheme
25
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
26
+
27
+ router = APIRouter()
28
+
29
+ # Admin user credentials (in production, use database)
30
+ ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin")
31
+ ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin")
32
+
33
+ def verify_password(plain_password, hashed_password):
34
+ return pwd_context.verify(plain_password, hashed_password)
35
+
36
+ def get_password_hash(password):
37
+ return pwd_context.hash(password)
38
+
39
+ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
40
+ to_encode = data.copy()
41
+ if expires_delta:
42
+ expire = datetime.utcnow() + expires_delta
43
+ else:
44
+ expire = datetime.utcnow() + timedelta(minutes=15)
45
+ to_encode.update({"exp": expire})
46
+ encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
47
+ return encoded_jwt
48
+
49
+ async def get_current_admin(token: str = Depends(oauth2_scheme)):
50
+ credentials_exception = HTTPException(
51
+ status_code=status.HTTP_401_UNAUTHORIZED,
52
+ detail="Could not validate credentials",
53
+ headers={"WWW-Authenticate": "Bearer"},
54
+ )
55
+ try:
56
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
57
+ username: str = payload.get("sub")
58
+ if username is None:
59
+ raise credentials_exception
60
+ except JWTError:
61
+ raise credentials_exception
62
+ if username != ADMIN_USERNAME:
63
+ raise credentials_exception
64
+ return username
65
+
66
+ @router.post("/token")
67
+ async def login(form_data: OAuth2PasswordRequestForm = Depends()):
68
+ if form_data.username != ADMIN_USERNAME or form_data.password != ADMIN_PASSWORD:
69
+ raise HTTPException(
70
+ status_code=status.HTTP_401_UNAUTHORIZED,
71
+ detail="Incorrect username or password",
72
+ headers={"WWW-Authenticate": "Bearer"},
73
+ )
74
+ access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
75
+ access_token = create_access_token(
76
+ data={"sub": form_data.username}, expires_delta=access_token_expires
77
+ )
78
+ return {"access_token": access_token, "token_type": "bearer"}
79
+
80
+ @router.post("/customers/", response_model=dict)
81
+ async def create_customer(
82
+ customer_data: dict,
83
+ db: Session = Depends(models.get_db),
84
+ current_admin: str = Depends(get_current_admin)
85
+ ):
86
+ # Generate API key
87
+ api_key = secrets.token_urlsafe(32)
88
+
89
+ # Create new customer
90
+ customer = models.Customer(
91
+ name=customer_data["name"],
92
+ company_name=customer_data["company_name"],
93
+ email=customer_data["email"],
94
+ api_key=api_key
95
+ )
96
+
97
+ db.add(customer)
98
+ db.commit()
99
+ db.refresh(customer)
100
+
101
+ return {
102
+ "id": customer.id,
103
+ "name": customer.name,
104
+ "company_name": customer.company_name,
105
+ "email": customer.email,
106
+ "api_key": customer.api_key
107
+ }
108
+
109
+ @router.get("/customers/", response_model=list)
110
+ async def list_customers(
111
+ db: Session = Depends(models.get_db),
112
+ current_admin: str = Depends(get_current_admin)
113
+ ):
114
+ customers = db.query(models.Customer).all()
115
+ return customers
116
+
117
+ @router.get("/customers/{customer_id}", response_model=dict)
118
+ async def get_customer(
119
+ customer_id: int,
120
+ db: Session = Depends(models.get_db),
121
+ current_admin: str = Depends(get_current_admin)
122
+ ):
123
+ customer = db.query(models.Customer).filter(models.Customer.id == customer_id).first()
124
+ if not customer:
125
+ raise HTTPException(status_code=404, detail="Customer not found")
126
+ return customer
127
+
128
+ @router.delete("/customers/{customer_id}")
129
+ async def delete_customer(
130
+ customer_id: int,
131
+ db: Session = Depends(models.get_db),
132
+ current_admin: str = Depends(get_current_admin)
133
+ ):
134
+ customer = db.query(models.Customer).filter(models.Customer.id == customer_id).first()
135
+ if not customer:
136
+ raise HTTPException(status_code=404, detail="Customer not found")
137
+ db.delete(customer)
138
+ db.commit()
139
+ return {"message": "Customer deleted successfully"}
app/models.py CHANGED
@@ -1,6 +1,6 @@
1
- from sqlalchemy import Column, Integer, String, DateTime, create_engine
2
  from sqlalchemy.ext.declarative import declarative_base
3
- from sqlalchemy.orm import sessionmaker
4
  from datetime import datetime
5
  import os
6
  from dotenv import load_dotenv
@@ -17,16 +17,38 @@ engine = create_engine(DATABASE_URL)
17
  # Create declarative base
18
  Base = declarative_base()
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  class CallRecord(Base):
21
  __tablename__ = "call_records"
22
 
23
  id = Column(String(36), primary_key=True)
 
 
 
24
  file_path = Column(String(255))
25
  transcription = Column(String(10000))
 
26
  sentiment = Column(String(20))
27
  keywords = Column(String(1000))
28
  created_at = Column(DateTime, default=datetime.utcnow)
29
  updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
 
 
 
30
 
31
  # Create all tables
32
  Base.metadata.create_all(engine)
 
1
+ from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean, create_engine
2
  from sqlalchemy.ext.declarative import declarative_base
3
+ from sqlalchemy.orm import sessionmaker, relationship
4
  from datetime import datetime
5
  import os
6
  from dotenv import load_dotenv
 
17
  # Create declarative base
18
  Base = declarative_base()
19
 
20
+ class Customer(Base):
21
+ __tablename__ = "customers"
22
+
23
+ id = Column(Integer, primary_key=True)
24
+ name = Column(String(100), nullable=False)
25
+ company_name = Column(String(100), nullable=False)
26
+ email = Column(String(100), unique=True, nullable=False)
27
+ api_key = Column(String(64), unique=True, nullable=False)
28
+ is_active = Column(Boolean, default=True)
29
+ created_at = Column(DateTime, default=datetime.utcnow)
30
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
31
+
32
+ # Relationship with call records
33
+ call_records = relationship("CallRecord", back_populates="customer")
34
+
35
  class CallRecord(Base):
36
  __tablename__ = "call_records"
37
 
38
  id = Column(String(36), primary_key=True)
39
+ customer_id = Column(Integer, ForeignKey("customers.id"), nullable=False)
40
+ caller_number = Column(String(20), nullable=False)
41
+ called_number = Column(String(20), nullable=False)
42
  file_path = Column(String(255))
43
  transcription = Column(String(10000))
44
+ summary = Column(String(1000))
45
  sentiment = Column(String(20))
46
  keywords = Column(String(1000))
47
  created_at = Column(DateTime, default=datetime.utcnow)
48
  updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
49
+
50
+ # Relationship with customer
51
+ customer = relationship("Customer", back_populates="call_records")
52
 
53
  # Create all tables
54
  Base.metadata.create_all(engine)
asterisk_dialplan.conf ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [from-internal]
2
+ ; Replace YOUR_API_KEY with the API key provided by vBot
3
+ exten => _X.,1,Answer()
4
+ same => n,Set(CALLERID=${CALLERID(num)})
5
+ same => n,Set(CALLED=${EXTEN})
6
+ same => n,Record(/tmp/call-${UNIQUEID}.wav,0,30)
7
+ same => n,System(curl -X POST "https://your-huggingface-space-url/api/v1/process-call" \
8
+ -H "api-key: YOUR_API_KEY" \
9
+ -H "caller-number: ${CALLERID}" \
10
+ -H "called-number: ${CALLED}" \
11
+ -F "file=@/tmp/call-${UNIQUEID}.wav")
12
+ same => n,System(rm /tmp/call-${UNIQUEID}.wav)
13
+ same => n,Dial(SIP/${EXTEN})
14
+ same => n,Hangup()
main.py CHANGED
@@ -1,11 +1,16 @@
1
- from fastapi import FastAPI, File, UploadFile, HTTPException, Depends
2
- from fastapi.security import OAuth2PasswordBearer
3
  from fastapi.middleware.cors import CORSMiddleware
 
4
  from datetime import datetime
5
  import os
6
  from dotenv import load_dotenv
7
- from typing import List
8
  import uuid
 
 
 
 
 
9
 
10
  # Load environment variables
11
  load_dotenv()
@@ -26,24 +31,33 @@ app.add_middleware(
26
  allow_headers=["*"],
27
  )
28
 
29
- # OAuth2 scheme for token authentication
30
- oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
 
 
31
 
32
- # Dependency for authentication
33
- async def get_current_user(token: str = Depends(oauth2_scheme)):
34
- # TODO: Implement proper JWT token validation
35
- if not token:
 
 
 
 
 
36
  raise HTTPException(
37
  status_code=401,
38
- detail="Could not validate credentials",
39
- headers={"WWW-Authenticate": "Bearer"},
40
  )
41
- return token
42
 
43
  @app.post("/api/v1/process-call")
44
  async def process_call(
45
  file: UploadFile = File(...),
46
- current_user: str = Depends(get_current_user)
 
 
 
47
  ):
48
  """
49
  Process a voice call recording file.
@@ -52,23 +66,61 @@ async def process_call(
52
  # Generate unique ID for this processing request
53
  process_id = str(uuid.uuid4())
54
 
55
- # TODO: Implement actual file processing logic
56
- # 1. Save the file temporarily
57
- # 2. Process audio using ML models
58
- # 3. Clean up temporary files
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
- # Mock response for now
61
  return {
62
  "id": process_id,
63
- "text": "Sample transcribed text",
64
- "sentiment": "neutral",
65
- "keywords": ["sample", "keywords"],
66
  "timestamp": datetime.utcnow().isoformat() + "Z"
67
  }
68
 
69
  except Exception as e:
70
  raise HTTPException(status_code=500, detail=str(e))
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  if __name__ == "__main__":
73
  import uvicorn
74
  host = os.getenv("HOST", "0.0.0.0")
 
1
+ from fastapi import FastAPI, File, UploadFile, HTTPException, Depends, Header
 
2
  from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.responses import JSONResponse
4
  from datetime import datetime
5
  import os
6
  from dotenv import load_dotenv
7
+ from typing import Optional
8
  import uuid
9
+ import tempfile
10
+ from transformers import pipeline
11
+ from sqlalchemy.orm import Session
12
+ import asyncio
13
+ from app import models, auth
14
 
15
  # Load environment variables
16
  load_dotenv()
 
31
  allow_headers=["*"],
32
  )
33
 
34
+ # Initialize Hugging Face models
35
+ transcriber = pipeline("automatic-speech-recognition", model="openai/whisper-base")
36
+ summarizer = pipeline("summarization", model="facebook/bart-large-cnn")
37
+ sentiment_analyzer = pipeline("sentiment-analysis", model="nlptown/bert-base-multilingual-uncased-sentiment")
38
 
39
+ # Include auth router
40
+ app.include_router(auth.router, prefix="/api/v1", tags=["auth"])
41
+
42
+ async def get_customer_by_api_key(api_key: str = Header(...), db: Session = Depends(models.get_db)):
43
+ customer = db.query(models.Customer).filter(
44
+ models.Customer.api_key == api_key,
45
+ models.Customer.is_active == True
46
+ ).first()
47
+ if not customer:
48
  raise HTTPException(
49
  status_code=401,
50
+ detail="Invalid or inactive API key"
 
51
  )
52
+ return customer
53
 
54
  @app.post("/api/v1/process-call")
55
  async def process_call(
56
  file: UploadFile = File(...),
57
+ caller_number: str = Header(...),
58
+ called_number: str = Header(...),
59
+ customer: models.Customer = Depends(get_customer_by_api_key),
60
+ db: Session = Depends(models.get_db)
61
  ):
62
  """
63
  Process a voice call recording file.
 
66
  # Generate unique ID for this processing request
67
  process_id = str(uuid.uuid4())
68
 
69
+ # Save the uploaded file temporarily
70
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as temp_file:
71
+ content = await file.read()
72
+ temp_file.write(content)
73
+ temp_file_path = temp_file.name
74
+
75
+ # Process audio using Hugging Face models
76
+ # Note: In production, these should be processed asynchronously
77
+ transcription = transcriber(temp_file_path)["text"]
78
+ summary = summarizer(transcription, max_length=130, min_length=30, do_sample=False)[0]["summary_text"]
79
+ sentiment = sentiment_analyzer(transcription)[0]["label"]
80
+
81
+ # Create call record
82
+ call_record = models.CallRecord(
83
+ id=process_id,
84
+ customer_id=customer.id,
85
+ caller_number=caller_number,
86
+ called_number=called_number,
87
+ file_path=temp_file_path,
88
+ transcription=transcription,
89
+ summary=summary,
90
+ sentiment=sentiment
91
+ )
92
+
93
+ db.add(call_record)
94
+ db.commit()
95
+
96
+ # Clean up temporary file
97
+ os.unlink(temp_file_path)
98
 
 
99
  return {
100
  "id": process_id,
101
+ "transcription": transcription,
102
+ "summary": summary,
103
+ "sentiment": sentiment,
104
  "timestamp": datetime.utcnow().isoformat() + "Z"
105
  }
106
 
107
  except Exception as e:
108
  raise HTTPException(status_code=500, detail=str(e))
109
 
110
+ @app.get("/api/v1/calls/{customer_id}")
111
+ async def list_calls(
112
+ customer_id: int,
113
+ db: Session = Depends(models.get_db),
114
+ current_admin: str = Depends(auth.get_current_admin)
115
+ ):
116
+ """
117
+ List all calls for a specific customer.
118
+ """
119
+ calls = db.query(models.CallRecord).filter(
120
+ models.CallRecord.customer_id == customer_id
121
+ ).all()
122
+ return calls
123
+
124
  if __name__ == "__main__":
125
  import uvicorn
126
  host = os.getenv("HOST", "0.0.0.0")
requirements.txt CHANGED
@@ -7,4 +7,9 @@ pymysql==1.1.0
7
  cryptography==42.0.2
8
  python-dotenv==1.0.0
9
  passlib[bcrypt]==1.7.4
10
- PyJWT==2.8.0
 
 
 
 
 
 
7
  cryptography==42.0.2
8
  python-dotenv==1.0.0
9
  passlib[bcrypt]==1.7.4
10
+ PyJWT==2.8.0
11
+ transformers==4.37.2
12
+ torch==2.2.0
13
+ soundfile==0.12.1
14
+ librosa==0.10.1
15
+ numpy==1.26.3