Skip to content

Commit 9a8c0d9

Browse files
adding support for the external call via proxy
1 parent 9ddc977 commit 9a8c0d9

9 files changed

Lines changed: 183 additions & 15 deletions

File tree

.env.example

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,15 @@ BREEZE_DEFAULT_SALES_TAB='SALES'
100100
# STT Prompt Configuration
101101
AUTOMATIC_OPENAI_STT_PROMPT="Transcribe Indian languages accurately. Recognize and render Indian numbers (lakhs, crores, etc.) in their natural spoken form. Ensure business and financial terms are captured with precision. Always include proper names, numbers, and technical terms exactly as spoken, without modification."
102102

103+
104+
# Proxy Configuration
105+
PROXY_HOST=localhost
106+
PROXY_PORT=80
107+
108+
# LangFuse Configuration
109+
ENABLE_LANGFUSE_PROMPTS=false
110+
LANGFUSE_SECRET_KEY=''
111+
LANGFUSE_PUBLIC_KEY=''
112+
LANGFUSE_BASEURL=https://us.cloud.langfuse.com
113+
AUTOMATIC_LANGFUSE_PROMPT_NAME=AUTOMATIC_VOICE_LANGFUSE_PROMPT
114+
AUTOMATIC_LANGFUSE_SYSTEM_PROMPT_LABEL=automatic_system_langfuse_prompt

app/agents/voice/automatic/services/mcp/automatic_client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from app.core.config import MCP_CLIENT_TIMEOUT
99
from app.core.logger import logger
10+
from app.utils.http_client import create_http_client
1011
from pipecat.adapters.schemas.tools_schema import ToolsSchema
1112
from pipecat.adapters.schemas.function_schema import FunctionSchema
1213
from app.agents.voice.automatic.types.models import (
@@ -27,7 +28,7 @@ def __init__(self, server_url: str, auth_token: str, context: Dict[str, Any]):
2728
self._server_url = server_url.strip()
2829
self._auth_token = auth_token
2930
self._context_b64 = base64.b64encode(json.dumps(context).encode()).decode()
30-
self._client = httpx.AsyncClient(timeout=MCP_CLIENT_TIMEOUT)
31+
self._client = create_http_client(timeout=MCP_CLIENT_TIMEOUT)
3132
self._demo_mode = context.get("enableDemoMode", False)
3233

3334
async def post(self, method: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:

app/agents/voice/automatic/tools/breeze/analytics.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from pipecat.services.llm_service import FunctionCallParams
99
from pipecat.adapters.schemas.function_schema import FunctionSchema
1010
from pipecat.adapters.schemas.tools_schema import ToolsSchema
11+
from app.utils.http_client import create_http_client
1112

1213
# These will be set by the initializer
1314
breeze_token: str | None = None
@@ -77,7 +78,7 @@ async def _make_breeze_request(params: FunctionCallParams, operational_tab: str)
7778
logger.info(f"Requesting Breeze {operational_tab} data with payload: {json.dumps(payload)}")
7879

7980
try:
80-
async with httpx.AsyncClient(timeout=30.0) as client:
81+
async with create_http_client(timeout=30.0) as client:
8182
response = await client.post(api_url, json=payload, headers=headers)
8283
response.raise_for_status() # Raise an exception for bad status codes
8384
response_json = response.json()
@@ -165,7 +166,7 @@ async def get_breeze_marketing_data(params: FunctionCallParams):
165166
logger.info(f"Requesting Breeze marketing data with payload: {json.dumps(payload)}")
166167

167168
try:
168-
async with httpx.AsyncClient() as client:
169+
async with create_http_client() as client:
169170
response = await client.post(api_url, json=payload, headers=headers)
170171
response.raise_for_status()
171172
response_json = response.json()
@@ -231,7 +232,7 @@ async def get_breeze_address_data(params: FunctionCallParams):
231232
logger.info(f"Requesting Breeze address data with payload: {json.dumps(payload)}")
232233

233234
try:
234-
async with httpx.AsyncClient() as client:
235+
async with create_http_client() as client:
235236
response = await client.post(api_url, json=payload, headers=headers)
236237
response.raise_for_status()
237238
response_json = response.json()

app/agents/voice/automatic/tools/breeze/utils.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from app.core.logger import logger
1515
from app.core.config import LIGHTHOUSE_APP_URL
16+
from app.utils.http_client import create_http_client
1617

1718
def safe_construct_url(url: str) -> Optional[urlparse]:
1819
"""
@@ -110,7 +111,7 @@ async def get_current_shop_config_data(shop_url: str) -> Dict[str, Any]:
110111

111112
try:
112113
logger.info(f"Fetching shop config from: {url}")
113-
async with httpx.AsyncClient(timeout=30.0) as client:
114+
async with create_http_client(timeout=30.0) as client:
114115
response = await client.get(url)
115116
response.raise_for_status()
116117
config_data = response.json()
@@ -155,7 +156,7 @@ async def patch_shop_config(
155156
"x-auth-token": breeze_token
156157
}
157158
try:
158-
async with httpx.AsyncClient(timeout=timeout) as client:
159+
async with create_http_client(timeout=timeout) as client:
159160
response = await client.patch(url, headers=headers, json=config_data)
160161
response.raise_for_status()
161162
return response.json()

app/agents/voice/automatic/tools/breeze_buddy/order.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from app.agents.voice.breeze_buddy.breeze.order_confirmation.types import BreezeOrderData
44
from app.core.config import ORDER_CONFIRMATION_ENDPOINT, ORDER_CONFIRMATION_TOKEN
55
from app.core.logger import logger
6+
from app.utils.http_client import create_http_client
67

78
async def initiate_order_confirmation_call(params: FunctionCallParams):
89
"""
@@ -26,7 +27,7 @@ async def initiate_order_confirmation_call(params: FunctionCallParams):
2627
# Assuming order_data can be directly converted to BreezeOrderData
2728
breeze_order_data = BreezeOrderData(**order_data)
2829

29-
async with httpx.AsyncClient() as client:
30+
async with create_http_client() as client:
3031
try:
3132
response = await client.post(url, headers=headers, json=breeze_order_data.model_dump(exclude_none=True))
3233
response.raise_for_status()

app/agents/voice/automatic/tools/juspay/analytics.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from pipecat.adapters.schemas.function_schema import FunctionSchema
1111
from pipecat.adapters.schemas.tools_schema import ToolsSchema
1212
from app.agents.voice.automatic.types.models import ApiFailure, ApiSuccess, GeniusApiResponse
13+
from app.utils.http_client import create_http_client
1314

1415
# This token will be set when the tools are initialized
1516
euler_token: str | None = None
@@ -92,7 +93,7 @@ async def _make_genius_api_request(params: FunctionCallParams, payload_details:
9293
f"Requesting Juspay Genius API with payload: {json.dumps(full_payload)}")
9394

9495
try:
95-
async with httpx.AsyncClient(timeout=10.0) as client:
96+
async with create_http_client(timeout=10.0) as client:
9697
response = await client.post(GENIUS_API_URL, json=full_payload, headers=headers)
9798
response.raise_for_status()
9899
response_text = response.text
@@ -264,7 +265,7 @@ async def list_offers_by_filter(params: FunctionCallParams):
264265

265266
logger.info(f"Requesting Euler offers list from: {endpoint} | Payload: {json.dumps(payload, indent=2)}")
266267

267-
async with httpx.AsyncClient(timeout=30.0) as client:
268+
async with create_http_client(timeout=30.0) as client:
268269
response = await client.post(endpoint, json=payload, headers=headers)
269270

270271
if response.status_code != 200:
@@ -581,7 +582,7 @@ async def create_euler_offer(params: FunctionCallParams):
581582

582583
logger.info(f"Making offer creation request to: {endpoint} | Payload: {json.dumps(api_payload, indent=2)}")
583584

584-
async with httpx.AsyncClient(timeout=10.0) as client:
585+
async with create_http_client(timeout=10.0) as client:
585586
response = await client.post(endpoint, json=api_payload, headers=headers)
586587

587588
if response.status_code == 200:
@@ -700,7 +701,7 @@ async def find_offer_by_code(offer_code: str) -> dict | None:
700701

701702
logger.info(f"Searching for offer with code '{offer_code}' using endpoint: {endpoint}")
702703

703-
async with httpx.AsyncClient(timeout=30.0) as client:
704+
async with create_http_client(timeout=30.0) as client:
704705
response = await client.post(endpoint, json=search_payload, headers=headers)
705706

706707
if response.status_code == 200:
@@ -776,7 +777,7 @@ async def delete_euler_offer(params: FunctionCallParams):
776777

777778
logger.info(f"Deleting offer with ID '{offer_id}' using endpoint: {delete_endpoint}")
778779

779-
async with httpx.AsyncClient(timeout=30.0) as client:
780+
async with create_http_client(timeout=30.0) as client:
780781
response = await client.post(delete_endpoint, json=delete_payload, headers=headers)
781782

782783
if response.status_code == 200:
@@ -927,7 +928,7 @@ async def update_euler_offer(params: FunctionCallParams):
927928

928929
logger.info(f"Using status-only endpoint: {status_endpoint}")
929930

930-
async with httpx.AsyncClient(timeout=30.0) as client:
931+
async with create_http_client(timeout=30.0) as client:
931932
response = await client.post(status_endpoint, json=status_payload, headers=headers)
932933

933934
if response.status_code == 200:
@@ -1139,7 +1140,7 @@ async def update_euler_offer(params: FunctionCallParams):
11391140
logger.info(f"Updating offer details with ID '{offer_id}' using endpoint: {update_endpoint}")
11401141
logger.info(f"Payload: {json.dumps(api_payload, indent=2)}")
11411142

1142-
async with httpx.AsyncClient(timeout=30.0) as client:
1143+
async with create_http_client(timeout=30.0) as client:
11431144
response = await client.put(update_endpoint, json=api_payload, headers=headers)
11441145

11451146
if response.status_code == 200:

app/agents/voice/automatic/tools/shopify_buddy_test/get_order.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from app.core.config import BREEZE_BUDDY_TEST_SHOPIFY_SHOP_URL, BREEZE_BUDDY_TEST_SHOPIFY_ADMIN_TOKEN
55
from app.core.logger import logger
66
from app.agents.voice.breeze_buddy.breeze.order_confirmation.types import BreezeOrderData, OrderData, OrderItem
7+
from app.utils.http_client import create_http_client
78

89
def _transform_shopify_to_breeze(order_node: dict, shop_name: str) -> BreezeOrderData:
910
"""Transforms a Shopify order node into a BreezeOrderData object."""
@@ -114,7 +115,7 @@ async def get_shopify_orders(params: FunctionCallParams):
114115
"""
115116
data = {"query": query, "variables": {}}
116117
logger.info(f"Requesting Shopify orders with payload: {json.dumps(data)}")
117-
async with httpx.AsyncClient() as client:
118+
async with create_http_client() as client:
118119
try:
119120
response = await client.post(url, headers=headers, data=json.dumps(data))
120121
response.raise_for_status()

app/core/config.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,27 @@ def get_required_env(var_name: str) -> str:
173173
"AUTOMATIC_OPENAI_STT_PROMPT",
174174
""
175175
)
176+
177+
EXOTEL_ACCOUNT_SID = os.getenv("EXOTEL_ACCOUNT_SID")
178+
EXOTEL_API_KEY = os.getenv("EXOTEL_API_KEY")
179+
EXOTEL_API_TOKEN = os.getenv("EXOTEL_API_TOKEN")
180+
EXOTEL_SUBDOMAIN = os.getenv("EXOTEL_SUBDOMAIN", "api.exotel.com")
181+
EXOTEL_FROM_NUMBER = os.getenv("EXOTEL_FROM_NUMBER")
182+
EXOTEL_ORDER_CONFIRMATION_WEBSOCKET_URL = os.getenv("EXOTEL_ORDER_CONFIRMATION_WEBSOCKET_URL", "")
183+
EXOTEL_APPLET_APP_ID = os.getenv("EXOTEL_APPLET_APP_ID", "1044183")
184+
185+
# Proxy Configuration
186+
PROXY_HOST = os.environ.get("PROXY_HOST")
187+
PROXY_PORT = os.environ.get("PROXY_PORT")
188+
189+
# LangFuse Configuration
190+
ENABLE_LANGFUSE_PROMPTS = os.environ.get("ENABLE_LANGFUSE_PROMPTS", "false").lower() == "true"
191+
LANGFUSE_SECRET_KEY = os.environ.get("LANGFUSE_SECRET_KEY", "")
192+
LANGFUSE_PUBLIC_KEY = os.environ.get("LANGFUSE_PUBLIC_KEY", "")
193+
LANGFUSE_BASEURL = os.environ.get("LANGFUSE_BASEURL", "https://us.cloud.langfuse.com")
194+
AUTOMATIC_LANGFUSE_PROMPT_NAME = os.environ.get("AUTOMATIC_LANGFUSE_PROMPT_NAME", "AUTOMATIC_VOICE_LANGFUSE_PROMPT")
195+
AUTOMATIC_LANGFUSE_SYSTEM_PROMPT_LABEL = os.environ.get("AUTOMATIC_LANGFUSE_SYSTEM_PROMPT_LABEL", "automatic_system_langfuse_prompt")
196+
197+
logger.info(f"LangFuse prompts enabled: {ENABLE_LANGFUSE_PROMPTS}")
198+
if ENABLE_LANGFUSE_PROMPTS:
199+
logger.info(f"LangFuse system prompt: {AUTOMATIC_LANGFUSE_PROMPT_NAME} (label: {AUTOMATIC_LANGFUSE_SYSTEM_PROMPT_LABEL})")

app/utils/http_client.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""
2+
HTTP Client Factory with Proxy Support
3+
4+
This module provides a centralized way to create httpx.AsyncClient instances
5+
with proxy configuration support. All external API calls should use this
6+
factory to ensure consistent proxy usage across the application.
7+
"""
8+
9+
import httpx
10+
from typing import Optional, Dict, Any
11+
from loguru import logger
12+
from app.core.config import PROXY_HOST, PROXY_PORT
13+
14+
15+
def get_proxy_config() -> Optional[str]:
16+
"""
17+
Get the proxy configuration string if proxy is configured.
18+
19+
Returns:
20+
str: Proxy URL in format "http://host:port" or None if not configured
21+
"""
22+
if PROXY_HOST and PROXY_PORT:
23+
proxy_url = f"http://{PROXY_HOST}:{PROXY_PORT}"
24+
logger.debug(f"Using proxy configuration: {proxy_url}")
25+
return proxy_url
26+
return None
27+
28+
29+
def create_http_client(
30+
timeout: Optional[float] = None,
31+
proxies: Optional[Dict[str, str]] = None,
32+
**kwargs: Any
33+
) -> httpx.AsyncClient:
34+
"""
35+
Create an httpx.AsyncClient with proxy support.
36+
37+
Args:
38+
timeout: Request timeout in seconds
39+
proxies: Custom proxy configuration (overrides global proxy)
40+
**kwargs: Additional arguments to pass to httpx.AsyncClient
41+
42+
Returns:
43+
httpx.AsyncClient: Configured HTTP client instance
44+
"""
45+
# Use custom proxies if provided, otherwise use global proxy config
46+
if proxies is None:
47+
proxy_url = get_proxy_config()
48+
if proxy_url:
49+
proxies = {
50+
"http://": proxy_url,
51+
"https://": proxy_url
52+
}
53+
54+
# Set default timeout if not provided
55+
if timeout is None:
56+
timeout = 30.0
57+
58+
client_kwargs = {
59+
"timeout": timeout,
60+
**kwargs
61+
}
62+
63+
# Add proxy configuration if available
64+
if proxies:
65+
# In httpx 0.28+, use 'proxy' parameter with a single URL
66+
proxy_url = proxies.get("https://") or proxies.get("http://")
67+
if proxy_url:
68+
client_kwargs["proxy"] = proxy_url
69+
logger.debug(f"Creating HTTP client with proxy: {proxy_url}")
70+
else:
71+
logger.debug("Creating HTTP client without proxy")
72+
else:
73+
logger.debug("Creating HTTP client without proxy")
74+
75+
return httpx.AsyncClient(**client_kwargs)
76+
77+
78+
async def make_request(
79+
method: str,
80+
url: str,
81+
timeout: Optional[float] = None,
82+
proxies: Optional[Dict[str, str]] = None,
83+
**kwargs: Any
84+
) -> httpx.Response:
85+
"""
86+
Make a single HTTP request with proxy support.
87+
88+
Args:
89+
method: HTTP method (GET, POST, PUT, DELETE, etc.)
90+
url: Request URL
91+
timeout: Request timeout in seconds
92+
proxies: Custom proxy configuration
93+
**kwargs: Additional arguments to pass to the request
94+
95+
Returns:
96+
httpx.Response: HTTP response
97+
"""
98+
async with create_http_client(timeout=timeout, proxies=proxies) as client:
99+
response = await client.request(method, url, **kwargs)
100+
return response
101+
102+
103+
# Convenience functions for common HTTP methods
104+
async def get(url: str, **kwargs: Any) -> httpx.Response:
105+
"""Make a GET request with proxy support."""
106+
return await make_request("GET", url, **kwargs)
107+
108+
109+
async def post(url: str, **kwargs: Any) -> httpx.Response:
110+
"""Make a POST request with proxy support."""
111+
return await make_request("POST", url, **kwargs)
112+
113+
114+
async def put(url: str, **kwargs: Any) -> httpx.Response:
115+
"""Make a PUT request with proxy support."""
116+
return await make_request("PUT", url, **kwargs)
117+
118+
119+
async def delete(url: str, **kwargs: Any) -> httpx.Response:
120+
"""Make a DELETE request with proxy support."""
121+
return await make_request("DELETE", url, **kwargs)
122+
123+
124+
async def patch(url: str, **kwargs: Any) -> httpx.Response:
125+
"""Make a PATCH request with proxy support."""
126+
return await make_request("PATCH", url, **kwargs)

0 commit comments

Comments
 (0)