Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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")
94 changes: 94 additions & 0 deletions service/crm_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import xmlrpc.client
from typing import Optional


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


self.common = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/common", allow_none=True)
self.uid = self.common.authenticate(db, username, password, {})
if not self.uid:
raise Exception("Authentication failed. Check credentials or DB name.")
Copy link

Copilot AI Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic Exception should be replaced with a more specific exception type, such as AuthenticationError or ConnectionError, to provide better error handling context.

Copilot uses AI. Check for mistakes.
self.models = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/object", allow_none=True)
Copy link

Copilot AI Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The authentication credentials are passed in plain text and stored in instance variables. Consider implementing secure credential handling and avoid storing sensitive data in class attributes.

Copilot uses AI. Check for mistakes.


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

def update_lead(self, lead_id: int, vals: dict):
return self.models.execute_kw(
self.db, self.uid, self.password,
"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.db, self.uid, self.password,
"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.db, self.uid, self.password,
"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.db, self.uid, self.password,
"res.partner", "write",
[[partner_id], vals]
)
202 changes: 201 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,199 @@ 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"}
Copy link

Copilot AI Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing timeout parameter for the HTTP request. Add a timeout to prevent the request from hanging indefinitely: requests.post(..., timeout=30).

Suggested change
headers={"Content-Type": "application/json"}
headers={"Content-Type": "application/json"},
timeout=30

Copilot uses AI. Check for mistakes.
)

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"}
)
Copy link

Copilot AI Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing timeout parameter for the HTTP request. Add a timeout to prevent the request from hanging indefinitely: requests.get(..., timeout=30).

Copilot uses AI. Check for mistakes.

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"}
Copy link

Copilot AI Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing timeout parameter for the HTTP request. Add a timeout to prevent the request from hanging indefinitely: requests.get(..., timeout=30).

Suggested change
headers={"Content-Type": "application/json"}
headers={"Content-Type": "application/json"},
timeout=30

Copilot uses AI. Check for mistakes.
)

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