Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion service/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
#model_id = os.getenv('MODEL_ID', 'large-v3')
model_id = os.getenv('MODEL_ID','small')
model_path = os.getenv('MODEL_PATH', './models')
ollama_host = os.getenv("OLLAMA_HOST", "http://ollama:11434")
ollama_host = os.getenv("OLLAMA_HOST", "http://localhost:11434")
ollama_model_name = os.getenv("OLLAMA_MODEL_NAME", "llama3.2")
open_ai_model_name = os.getenv("OPENAI_MODEL_NAME", "gpt-4")
open_ai_temperature = os.getenv("OPENAI_TEMPERATURE", 0.2)

odoo_url = os.getenv("ODOO_URL")
odoo_db = os.getenv("ODOO_DB")
odoo_username = os.getenv("ODOO_USERNAME")
odoo_password = os.getenv("ODOO_PASSWORD")
104 changes: 104 additions & 0 deletions service/crm_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import xmlrpc.client
from typing import Optional


class AuthenticationError(Exception):
"""Raised when authentication with Odoo fails."""


class OdooCRMClient:
def __init__(self, url: str, db: str, username: str, password: str):
self.url = url
self.db = db
self.username = username

self.common = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/common", allow_none=True)
uid = self.common.authenticate(db, username, password, {})

if not uid:
raise AuthenticationError("Authentication failed. Check credentials or DB name.")

self.uid = uid
self.models = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/object", allow_none=True)

# Store auth securely (don’t keep password separately)
self._auth = (db, uid, password)

def create_lead(self, name: str, email: Optional[str], phone: Optional[str], lead_type: str = "opportunity"):
return self.models.execute_kw(
*self._auth,
"crm.lead", "create",
[{
"name": name,
"contact_name": name,
"email_from": email,
"phone": phone,
"type": lead_type,
}]
)

def update_lead(self, lead_id: int, vals: dict):
return self.models.execute_kw(
*self._auth,
"crm.lead", "write",
[[lead_id], vals]
)

def add_internal_note(self, lead_id: int, note_text: str):
return self.update_lead(lead_id, {"description": note_text})

def add_chatter_note(self, lead_id: int, note_text: str):
return self.models.execute_kw(
*self._auth,
"mail.message", "create",
[{
"model": "crm.lead",
"res_id": lead_id,
"body": note_text,
"message_type": "comment",
"subtype_id": 2,
}]
)

def add_contact_details(self, lead_id: int, contact_name: Optional[str] = None,
email: Optional[str] = None, phone: Optional[str] = None):
vals = {}
if contact_name:
vals["name"] = contact_name
if email:
vals["email"] = email
if phone:
vals["phone"] = phone

partner_id = self.models.execute_kw(
*self._auth,
"res.partner", "create",
[vals]
)

self.update_lead(lead_id, {"partner_id": partner_id})
return partner_id

def update_contact_address(self, partner_id: int, street: Optional[str] = None,
street2: Optional[str] = None, city: Optional[str] = None,
state_id: Optional[int] = None, zip_code: Optional[str] = None,
country_id: Optional[int] = None):
vals = {}
if street:
vals["street"] = street
if street2:
vals["street2"] = street2
if city:
vals["city"] = city
if state_id:
vals["state_id"] = state_id
if zip_code:
vals["zip"] = zip_code
if country_id:
vals["country_id"] = country_id

return self.models.execute_kw(
*self._auth,
"res.partner", "write",
[[partner_id], vals]
)
204 changes: 203 additions & 1 deletion service/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@
from audio_service import translate_with_whisper_timestamped, translate_with_whisper_from_upload
from detect_intent import detect_intent_with_llama, format_intent_response
from summarizer import summarize_using_openai
from summarizer import summarize_using_ollama
from summarizer import summarize_using_ollama, extract_contact_detailed_using_ollama
from pydantic import BaseModel
import traceback
from util import generate_timestamp_json
from fastapi_versionizer.versionizer import Versionizer, api_version
import json
from core_banking_mock import router as core_banking_mock_router
import os
import requests
from config import odoo_url, odoo_db, odoo_username, odoo_password
from crm_client import OdooCRMClient

app = FastAPI()

Expand Down Expand Up @@ -139,3 +143,201 @@ async def transcribe_intent(audio: UploadFile = File(...), session_id: str = For
except Exception as e:
logger.info(traceback.format_exc())
return JSONResponse(content={"message": str(e)}, status_code=500)
# async def save_crm_lead_data(lead_id, file_path, translation, extracted_data, summary, user_id=None, transcription_id=None):

# """
# Save CRM lead data to the database through the Next.js API.
# """
# try:
# # Get base URL from environment or use default
# api_base_url = os.environ.get("NEXT_API_BASE_URL", "http://localhost:3000")

# # Extract just the filename from the file path
# file_name = os.path.basename(file_path)

# # Prepare the data to send
# crm_lead_data = {
# "leadId": str(lead_id),
# "crmUrl": odoo_url, # Using the odoo_url from config
# "fileName": file_name,
# "transcriptionId": transcription_id, # This might be None if not provided
# "extractedData": extracted_data,
# "translation": translation,
# "userId": user_id, # This might be None if not provided
# "isDefault": False # Adding the default field set to false
# }

# # Make the API call
# response = requests.post(
# f"{api_base_url}/api/crm-leads",
# json=crm_lead_data,
# headers={"Content-Type": "application/json"}
# )

# if response.status_code == 200:
# logger.info(f"CRM lead data saved successfully for lead_id={lead_id}")
# return response.json()
# else:
# logger.error(f"Failed to save CRM lead data: {response.status_code} - {response.text}")
# return None

# except Exception as e:
# logger.error(f"Exception saving CRM lead data: {str(e)}")
# return None



# Add this function to retrieve default CRM leads
async def get_default_crm_leads():
"""
Fetch CRM leads that are marked as default.
"""
try:
api_base_url = os.environ.get("NEXT_API_BASE_URL", "http://localhost:3000")

response = requests.get(
f"{api_base_url}/api/crm-leads/default",
headers={"Content-Type": "application/json"},
timeout=30
)

if response.status_code == 200:
return response.json()
else:
logger.error(f"Failed to get default CRM leads: {response.status_code}")
return None
except Exception as e:
logger.error(f"Error fetching default CRM leads: {str(e)}")
return None

# Add a route to expose this functionality
@app.get("/crm-leads/default")
async def fetch_default_crm_leads():
try:
result = await get_default_crm_leads()
if result and result.get("success"):
return JSONResponse(content=result, status_code=200)
else:
return JSONResponse(
content={"message": "Failed to retrieve default CRM leads"},
status_code=500
)
except Exception as e:
logger.error(f"Error in fetch_default_crm_leads: {str(e)}")
return JSONResponse(content={"message": str(e)}, status_code=500)
# Add this new, simpler endpoint

@app.get("/crm-lead/{lead_id}")
async def get_crm_lead_direct(lead_id: str):
"""
Get CRM lead data directly by ID from the database.
Simple direct lookup without complex routing.
"""
try:
api_base_url = os.environ.get("NEXT_API_BASE_URL", "http://localhost:3000")

# Make a direct GET request to a simple endpoint
simple_url = f"{api_base_url}/api/crm-leads/simple/{lead_id}"
logger.info(f"Making GET request to: {simple_url}")

response = requests.get(
simple_url,
headers={"Content-Type": "application/json"},
timeout=30
)

if response.status_code == 200:
return JSONResponse(content=response.json(), status_code=200)
else:
return JSONResponse(
content={"message": "CRM lead not found", "id": lead_id},
status_code=404
)

except Exception as e:
logger.error(f"Error retrieving CRM lead: {str(e)}")
return JSONResponse(content={"message": str(e)}, status_code=500)

@app.post("/upload-crm-audio")
async def upload_crm_audio(body: Body):
try:
if body.audio_file_link == "":
return JSONResponse(status_code=400, content={"message":"Invalid file link"})

translation = translate_with_whisper_timestamped(body.audio_file_link)

# Extract detected language
detected_language = translation.get("detected_language", "unknown")

logger.info("translation done")
summary = summarize_using_ollama(translation["text"])

logger.info("summary done")

# Pass the translation object and detected_language to generate_timestamp_json
result = generate_timestamp_json(translation, summary, detected_language)


contact_info = extract_contact_detailed_using_ollama(translation["text"]) if "text" in translation else {"name": None, "phone": None, "address": None}

logger.info(result)

# Fire-and-forget CRM sync (do not block response)
lead_id = None
try:
if odoo_url and odoo_db and odoo_username and odoo_password:
client = OdooCRMClient(odoo_url, odoo_db, odoo_username, odoo_password)
lead_id = client.create_lead(
name=contact_info.get("name") or "Unknown",
email=None,
phone=contact_info.get("phone"),
)
logger.info(f"CRM: Lead created lead_id={lead_id}")
partner_id = client.add_contact_details(lead_id, contact_info.get("name"), None, contact_info.get("phone"))
logger.info(f"CRM: Partner created/linked partner_id={partner_id} to lead_id={lead_id}")

# Update: Use the complete street information from LLM extraction
street = contact_info.get("street")
logger.info(f"CRM: - Street: '{street}'")


street2 = None
city = contact_info.get("city")
state = contact_info.get("state")
zip_code = contact_info.get("zip")
country = contact_info.get("country")

logger.info(f"CRM: Address components street={street}, city={city}")

updated = client.update_contact_address(
partner_id,
street=street,
street2=street2,
city=city,
state_id=None,
zip_code=zip_code,
country_id=None
)
logger.info(f"CRM: Address update result={updated} for partner_id={partner_id}")

# Add CRM data to the result
result["leadId"] = str(lead_id)
result["crmUrl"] = odoo_url
result["extractedData"] = contact_info
result["transcriptionId"] = None # This will be determined when transcription is saved
result["translation"] = translation["text"]
result["userId"] = None # This will be set by frontend
result["isDefault"] = False


else:
logger.info("CRM: Odoo credentials not configured; skipping CRM sync")
except Exception as e:
logger.info(f"CRM: Exception during sync: {e}")

return JSONResponse(content=result, status_code=200)

except Exception as e:
logger.info(traceback.format_exc())
return JSONResponse(content={"message": str(e)}, status_code=500)

Loading